@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,735 @@
|
|
|
1
|
+
import { invalidOptions } from './errors';
|
|
2
|
+
import { decodeBase64, decodeUtf8, encodeBase64, encodeUtf8 } from './helpers';
|
|
3
|
+
import type {
|
|
4
|
+
CommonMqttOptions,
|
|
5
|
+
DeliveryOptions,
|
|
6
|
+
MqttOptions,
|
|
7
|
+
PublishOptions,
|
|
8
|
+
ReconnectOptions,
|
|
9
|
+
SubscribeOptions,
|
|
10
|
+
TlsOptions,
|
|
11
|
+
Transport,
|
|
12
|
+
UserProperty,
|
|
13
|
+
WillOptions,
|
|
14
|
+
} from './types';
|
|
15
|
+
import type {
|
|
16
|
+
WirePublishRequest,
|
|
17
|
+
WireSessionOptions,
|
|
18
|
+
WireSubscribeOptions,
|
|
19
|
+
WireUserProperty,
|
|
20
|
+
} from './NativeReactNativeMqtt';
|
|
21
|
+
|
|
22
|
+
const ownKeys = (value: object): string[] => Object.keys(value);
|
|
23
|
+
function object(value: unknown, name: string): Record<string, unknown> {
|
|
24
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
25
|
+
throw invalidOptions(`${name} must be an object`);
|
|
26
|
+
}
|
|
27
|
+
return value as Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
function exactKeys(
|
|
30
|
+
value: object,
|
|
31
|
+
allowed: readonly string[],
|
|
32
|
+
name: string
|
|
33
|
+
): void {
|
|
34
|
+
const unknown = ownKeys(value).find((key) => !allowed.includes(key));
|
|
35
|
+
if (unknown !== undefined)
|
|
36
|
+
throw invalidOptions(`${name}.${unknown} is not supported`);
|
|
37
|
+
}
|
|
38
|
+
function stringValue(value: unknown, name: string, allowEmpty = true): string {
|
|
39
|
+
if (typeof value !== 'string' || (!allowEmpty && value.length === 0))
|
|
40
|
+
throw invalidOptions(`${name} must be a string`);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function wellFormedMqttText(value: string): boolean {
|
|
44
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
45
|
+
const code = value.charCodeAt(index);
|
|
46
|
+
if (code === 0 || (code >= 0xd800 && code <= 0xdfff)) {
|
|
47
|
+
if (code < 0xdc00 && index + 1 < value.length) {
|
|
48
|
+
const next = value.charCodeAt(index + 1);
|
|
49
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
50
|
+
index += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
function mqttString(value: unknown, name: string, allowEmpty = true): string {
|
|
60
|
+
const result = stringValue(value, name, allowEmpty);
|
|
61
|
+
if (!wellFormedMqttText(result))
|
|
62
|
+
throw invalidOptions(`${name} is not a valid MQTT UTF-8 string`);
|
|
63
|
+
if (encodeUtf8(result).length > 65535)
|
|
64
|
+
throw invalidOptions(`${name} exceeds the MQTT UTF-8 string limit`);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function boolValue(value: unknown, name: string): boolean {
|
|
68
|
+
if (typeof value !== 'boolean')
|
|
69
|
+
throw invalidOptions(`${name} must be a boolean`);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function integer(
|
|
73
|
+
value: unknown,
|
|
74
|
+
name: string,
|
|
75
|
+
min: number,
|
|
76
|
+
max: number
|
|
77
|
+
): number {
|
|
78
|
+
if (
|
|
79
|
+
typeof value !== 'number' ||
|
|
80
|
+
!Number.isSafeInteger(value) ||
|
|
81
|
+
value < min ||
|
|
82
|
+
value > max
|
|
83
|
+
)
|
|
84
|
+
throw invalidOptions(`${name} must be an integer in ${min}..${max}`);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function ratio(value: unknown, name: string): number {
|
|
88
|
+
if (
|
|
89
|
+
typeof value !== 'number' ||
|
|
90
|
+
!Number.isFinite(value) ||
|
|
91
|
+
value < 0 ||
|
|
92
|
+
value > 1
|
|
93
|
+
)
|
|
94
|
+
throw invalidOptions(`${name} must be a number in 0..1`);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function propertyPairs(value: unknown, name: string): UserProperty[] {
|
|
98
|
+
if (!Array.isArray(value)) throw invalidOptions(`${name} must be an array`);
|
|
99
|
+
return value.map((item, index) => {
|
|
100
|
+
const pair = object(item, `${name}[${index}]`);
|
|
101
|
+
exactKeys(pair, ['key', 'value'], `${name}[${index}]`);
|
|
102
|
+
return {
|
|
103
|
+
key: mqttString(pair.key, `${name}[${index}].key`),
|
|
104
|
+
value: mqttString(pair.value, `${name}[${index}].value`),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function base64(value: unknown, name: string): string {
|
|
109
|
+
const text = stringValue(value, name);
|
|
110
|
+
decodeBase64(text);
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
function validateTls(value: unknown): TlsOptions | undefined {
|
|
114
|
+
if (value === undefined) return undefined;
|
|
115
|
+
const tls = object(value, 'tls');
|
|
116
|
+
exactKeys(
|
|
117
|
+
tls,
|
|
118
|
+
[
|
|
119
|
+
'caBase64',
|
|
120
|
+
'p12Base64',
|
|
121
|
+
'p12Password',
|
|
122
|
+
'insecure',
|
|
123
|
+
'allowInvalidHostnameForTesting',
|
|
124
|
+
],
|
|
125
|
+
'tls'
|
|
126
|
+
);
|
|
127
|
+
const result: TlsOptions = {};
|
|
128
|
+
if (tls.caBase64 !== undefined)
|
|
129
|
+
result.caBase64 = base64(tls.caBase64, 'tls.caBase64');
|
|
130
|
+
if (tls.p12Base64 !== undefined)
|
|
131
|
+
result.p12Base64 = base64(tls.p12Base64, 'tls.p12Base64');
|
|
132
|
+
if (tls.p12Password !== undefined)
|
|
133
|
+
result.p12Password = stringValue(tls.p12Password, 'tls.p12Password');
|
|
134
|
+
if (tls.insecure !== undefined)
|
|
135
|
+
result.insecure = boolValue(tls.insecure, 'tls.insecure');
|
|
136
|
+
if (tls.allowInvalidHostnameForTesting !== undefined)
|
|
137
|
+
result.allowInvalidHostnameForTesting = boolValue(
|
|
138
|
+
tls.allowInvalidHostnameForTesting,
|
|
139
|
+
'tls.allowInvalidHostnameForTesting'
|
|
140
|
+
);
|
|
141
|
+
if (result.p12Password !== undefined && result.p12Base64 === undefined)
|
|
142
|
+
throw invalidOptions('tls.p12Password requires tls.p12Base64');
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
function validateReconnect(value: unknown): ReconnectOptions {
|
|
146
|
+
if (value === undefined)
|
|
147
|
+
return {
|
|
148
|
+
enabled: true,
|
|
149
|
+
initialDelayMs: 1000,
|
|
150
|
+
maxDelayMs: 30000,
|
|
151
|
+
jitterRatio: 0.2,
|
|
152
|
+
};
|
|
153
|
+
const reconnect = object(value, 'reconnect');
|
|
154
|
+
exactKeys(
|
|
155
|
+
reconnect,
|
|
156
|
+
['enabled', 'initialDelayMs', 'maxDelayMs', 'jitterRatio'],
|
|
157
|
+
'reconnect'
|
|
158
|
+
);
|
|
159
|
+
const result: ReconnectOptions = {
|
|
160
|
+
enabled:
|
|
161
|
+
reconnect.enabled === undefined
|
|
162
|
+
? true
|
|
163
|
+
: boolValue(reconnect.enabled, 'reconnect.enabled'),
|
|
164
|
+
initialDelayMs:
|
|
165
|
+
reconnect.initialDelayMs === undefined
|
|
166
|
+
? 1000
|
|
167
|
+
: integer(
|
|
168
|
+
reconnect.initialDelayMs,
|
|
169
|
+
'reconnect.initialDelayMs',
|
|
170
|
+
1,
|
|
171
|
+
Number.MAX_SAFE_INTEGER
|
|
172
|
+
),
|
|
173
|
+
maxDelayMs:
|
|
174
|
+
reconnect.maxDelayMs === undefined
|
|
175
|
+
? 30000
|
|
176
|
+
: integer(
|
|
177
|
+
reconnect.maxDelayMs,
|
|
178
|
+
'reconnect.maxDelayMs',
|
|
179
|
+
1,
|
|
180
|
+
Number.MAX_SAFE_INTEGER
|
|
181
|
+
),
|
|
182
|
+
jitterRatio:
|
|
183
|
+
reconnect.jitterRatio === undefined
|
|
184
|
+
? 0.2
|
|
185
|
+
: ratio(reconnect.jitterRatio, 'reconnect.jitterRatio'),
|
|
186
|
+
};
|
|
187
|
+
if ((result.maxDelayMs ?? 0) < (result.initialDelayMs ?? 0))
|
|
188
|
+
throw invalidOptions('reconnect.maxDelayMs must be >= initialDelayMs');
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
function validateDelivery(value: unknown): DeliveryOptions {
|
|
192
|
+
if (value === undefined)
|
|
193
|
+
return {
|
|
194
|
+
maxQueuedMessages: 1024,
|
|
195
|
+
maxQueuedBytes: 8388608,
|
|
196
|
+
maxMessageBytes: 1048576,
|
|
197
|
+
batchMaxMessages: 64,
|
|
198
|
+
batchMaxBytes: 262144,
|
|
199
|
+
batchAckTimeoutMs: 15000,
|
|
200
|
+
overflowPolicy: 'disconnect',
|
|
201
|
+
};
|
|
202
|
+
const delivery = object(value, 'delivery');
|
|
203
|
+
exactKeys(
|
|
204
|
+
delivery,
|
|
205
|
+
[
|
|
206
|
+
'maxQueuedMessages',
|
|
207
|
+
'maxQueuedBytes',
|
|
208
|
+
'maxMessageBytes',
|
|
209
|
+
'batchMaxMessages',
|
|
210
|
+
'batchMaxBytes',
|
|
211
|
+
'batchAckTimeoutMs',
|
|
212
|
+
'overflowPolicy',
|
|
213
|
+
],
|
|
214
|
+
'delivery'
|
|
215
|
+
);
|
|
216
|
+
const result: DeliveryOptions = {
|
|
217
|
+
maxQueuedMessages:
|
|
218
|
+
delivery.maxQueuedMessages === undefined
|
|
219
|
+
? 1024
|
|
220
|
+
: integer(
|
|
221
|
+
delivery.maxQueuedMessages,
|
|
222
|
+
'delivery.maxQueuedMessages',
|
|
223
|
+
1,
|
|
224
|
+
Number.MAX_SAFE_INTEGER
|
|
225
|
+
),
|
|
226
|
+
maxQueuedBytes:
|
|
227
|
+
delivery.maxQueuedBytes === undefined
|
|
228
|
+
? 8388608
|
|
229
|
+
: integer(
|
|
230
|
+
delivery.maxQueuedBytes,
|
|
231
|
+
'delivery.maxQueuedBytes',
|
|
232
|
+
1,
|
|
233
|
+
Number.MAX_SAFE_INTEGER
|
|
234
|
+
),
|
|
235
|
+
maxMessageBytes:
|
|
236
|
+
delivery.maxMessageBytes === undefined
|
|
237
|
+
? 1048576
|
|
238
|
+
: integer(
|
|
239
|
+
delivery.maxMessageBytes,
|
|
240
|
+
'delivery.maxMessageBytes',
|
|
241
|
+
1,
|
|
242
|
+
Number.MAX_SAFE_INTEGER
|
|
243
|
+
),
|
|
244
|
+
batchMaxMessages:
|
|
245
|
+
delivery.batchMaxMessages === undefined
|
|
246
|
+
? 64
|
|
247
|
+
: integer(
|
|
248
|
+
delivery.batchMaxMessages,
|
|
249
|
+
'delivery.batchMaxMessages',
|
|
250
|
+
1,
|
|
251
|
+
Number.MAX_SAFE_INTEGER
|
|
252
|
+
),
|
|
253
|
+
batchMaxBytes:
|
|
254
|
+
delivery.batchMaxBytes === undefined
|
|
255
|
+
? 262144
|
|
256
|
+
: integer(
|
|
257
|
+
delivery.batchMaxBytes,
|
|
258
|
+
'delivery.batchMaxBytes',
|
|
259
|
+
1,
|
|
260
|
+
Number.MAX_SAFE_INTEGER
|
|
261
|
+
),
|
|
262
|
+
batchAckTimeoutMs:
|
|
263
|
+
delivery.batchAckTimeoutMs === undefined
|
|
264
|
+
? 15000
|
|
265
|
+
: integer(
|
|
266
|
+
delivery.batchAckTimeoutMs,
|
|
267
|
+
'delivery.batchAckTimeoutMs',
|
|
268
|
+
1,
|
|
269
|
+
Number.MAX_SAFE_INTEGER
|
|
270
|
+
),
|
|
271
|
+
overflowPolicy:
|
|
272
|
+
delivery.overflowPolicy === undefined
|
|
273
|
+
? 'disconnect'
|
|
274
|
+
: delivery.overflowPolicy === 'drop-newest'
|
|
275
|
+
? 'drop-newest'
|
|
276
|
+
: delivery.overflowPolicy === 'disconnect'
|
|
277
|
+
? 'disconnect'
|
|
278
|
+
: undefined,
|
|
279
|
+
};
|
|
280
|
+
if (result.overflowPolicy === undefined)
|
|
281
|
+
throw invalidOptions('delivery.overflowPolicy is invalid');
|
|
282
|
+
if ((result.maxMessageBytes ?? 0) > (result.maxQueuedBytes ?? 0))
|
|
283
|
+
throw invalidOptions('delivery.maxMessageBytes must be <= maxQueuedBytes');
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
function validateWill(value: unknown): WillOptions | undefined {
|
|
287
|
+
if (value === undefined) return undefined;
|
|
288
|
+
const will = object(value, 'will');
|
|
289
|
+
exactKeys(will, ['topic', 'payloadBase64', 'qos', 'retained'], 'will');
|
|
290
|
+
const result: WillOptions = {
|
|
291
|
+
topic: mqttString(will.topic, 'will.topic', false),
|
|
292
|
+
payloadBase64: base64(will.payloadBase64, 'will.payloadBase64'),
|
|
293
|
+
qos:
|
|
294
|
+
will.qos === undefined
|
|
295
|
+
? 0
|
|
296
|
+
: (integer(will.qos, 'will.qos', 0, 2) as WillOptions['qos']),
|
|
297
|
+
retained:
|
|
298
|
+
will.retained === undefined
|
|
299
|
+
? false
|
|
300
|
+
: boolValue(will.retained, 'will.retained'),
|
|
301
|
+
};
|
|
302
|
+
validateTopicName(result.topic);
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
function common(value: Record<string, unknown>): CommonMqttOptions {
|
|
306
|
+
exactKeys(
|
|
307
|
+
value,
|
|
308
|
+
[
|
|
309
|
+
'host',
|
|
310
|
+
'port',
|
|
311
|
+
'transport',
|
|
312
|
+
'path',
|
|
313
|
+
'clientId',
|
|
314
|
+
'username',
|
|
315
|
+
'password',
|
|
316
|
+
'keepAliveSeconds',
|
|
317
|
+
'connectTimeoutMs',
|
|
318
|
+
'operationTimeoutMs',
|
|
319
|
+
'disconnectTimeoutMs',
|
|
320
|
+
'reconnect',
|
|
321
|
+
'tls',
|
|
322
|
+
'will',
|
|
323
|
+
'delivery',
|
|
324
|
+
'protocolVersion',
|
|
325
|
+
'cleanSession',
|
|
326
|
+
'cleanStart',
|
|
327
|
+
'sessionExpiryIntervalSeconds',
|
|
328
|
+
'receiveMaximum',
|
|
329
|
+
'userProperties',
|
|
330
|
+
],
|
|
331
|
+
'options'
|
|
332
|
+
);
|
|
333
|
+
const transport = value.transport === undefined ? 'tcp' : value.transport;
|
|
334
|
+
if (
|
|
335
|
+
transport !== 'tcp' &&
|
|
336
|
+
transport !== 'tls' &&
|
|
337
|
+
transport !== 'ws' &&
|
|
338
|
+
transport !== 'wss'
|
|
339
|
+
)
|
|
340
|
+
throw invalidOptions('transport is invalid');
|
|
341
|
+
const result: CommonMqttOptions = {
|
|
342
|
+
host: stringValue(value.host, 'host', false),
|
|
343
|
+
port: integer(value.port, 'port', 1, 65535),
|
|
344
|
+
transport: transport as Transport,
|
|
345
|
+
path:
|
|
346
|
+
value.path === undefined
|
|
347
|
+
? undefined
|
|
348
|
+
: stringValue(value.path, 'path', false),
|
|
349
|
+
clientId:
|
|
350
|
+
value.clientId === undefined
|
|
351
|
+
? undefined
|
|
352
|
+
: mqttString(value.clientId, 'clientId'),
|
|
353
|
+
username:
|
|
354
|
+
value.username === undefined
|
|
355
|
+
? undefined
|
|
356
|
+
: mqttString(value.username, 'username'),
|
|
357
|
+
password:
|
|
358
|
+
value.password === undefined
|
|
359
|
+
? undefined
|
|
360
|
+
: mqttString(value.password, 'password'),
|
|
361
|
+
keepAliveSeconds:
|
|
362
|
+
value.keepAliveSeconds === undefined
|
|
363
|
+
? 60
|
|
364
|
+
: integer(value.keepAliveSeconds, 'keepAliveSeconds', 0, 65535),
|
|
365
|
+
connectTimeoutMs:
|
|
366
|
+
value.connectTimeoutMs === undefined
|
|
367
|
+
? 30000
|
|
368
|
+
: integer(
|
|
369
|
+
value.connectTimeoutMs,
|
|
370
|
+
'connectTimeoutMs',
|
|
371
|
+
1,
|
|
372
|
+
Number.MAX_SAFE_INTEGER
|
|
373
|
+
),
|
|
374
|
+
operationTimeoutMs:
|
|
375
|
+
value.operationTimeoutMs === undefined
|
|
376
|
+
? 30000
|
|
377
|
+
: integer(
|
|
378
|
+
value.operationTimeoutMs,
|
|
379
|
+
'operationTimeoutMs',
|
|
380
|
+
1,
|
|
381
|
+
Number.MAX_SAFE_INTEGER
|
|
382
|
+
),
|
|
383
|
+
disconnectTimeoutMs:
|
|
384
|
+
value.disconnectTimeoutMs === undefined
|
|
385
|
+
? 5000
|
|
386
|
+
: integer(
|
|
387
|
+
value.disconnectTimeoutMs,
|
|
388
|
+
'disconnectTimeoutMs',
|
|
389
|
+
1,
|
|
390
|
+
Number.MAX_SAFE_INTEGER
|
|
391
|
+
),
|
|
392
|
+
reconnect: validateReconnect(value.reconnect),
|
|
393
|
+
tls: validateTls(value.tls),
|
|
394
|
+
will: validateWill(value.will),
|
|
395
|
+
delivery: validateDelivery(value.delivery),
|
|
396
|
+
};
|
|
397
|
+
if (
|
|
398
|
+
(result.path !== undefined) !==
|
|
399
|
+
(transport === 'ws' || transport === 'wss') &&
|
|
400
|
+
result.path !== undefined
|
|
401
|
+
)
|
|
402
|
+
throw invalidOptions('path is only valid for ws/wss');
|
|
403
|
+
if ((transport === 'ws' || transport === 'wss') && result.path === undefined)
|
|
404
|
+
result.path = '/mqtt';
|
|
405
|
+
if (result.tls !== undefined && transport !== 'tls' && transport !== 'wss')
|
|
406
|
+
throw invalidOptions('tls options are only valid for tls/wss');
|
|
407
|
+
if (result.password !== undefined && result.username === undefined)
|
|
408
|
+
throw invalidOptions('password requires username');
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
export function normalizeOptions(input: MqttOptions): MqttOptions {
|
|
412
|
+
const value = object(input, 'options');
|
|
413
|
+
const result = common(value);
|
|
414
|
+
if (
|
|
415
|
+
result.clientId !== undefined &&
|
|
416
|
+
encodeUtf8(result.clientId).length > 65535
|
|
417
|
+
)
|
|
418
|
+
throw invalidOptions('clientId exceeds the MQTT UTF-8 string limit');
|
|
419
|
+
const protocolVersion =
|
|
420
|
+
value.protocolVersion === undefined ? 4 : value.protocolVersion;
|
|
421
|
+
if (protocolVersion !== 4 && protocolVersion !== 5)
|
|
422
|
+
throw invalidOptions('protocolVersion must be 4 or 5');
|
|
423
|
+
if (protocolVersion === 4) {
|
|
424
|
+
if (result.clientId === '')
|
|
425
|
+
throw invalidOptions('empty clientId is only valid for MQTT5 cleanStart');
|
|
426
|
+
if (
|
|
427
|
+
value.cleanStart !== undefined ||
|
|
428
|
+
value.sessionExpiryIntervalSeconds !== undefined ||
|
|
429
|
+
value.receiveMaximum !== undefined ||
|
|
430
|
+
value.userProperties !== undefined
|
|
431
|
+
)
|
|
432
|
+
throw invalidOptions('MQTT5 options are not valid for protocolVersion 4');
|
|
433
|
+
return {
|
|
434
|
+
...result,
|
|
435
|
+
protocolVersion: 4,
|
|
436
|
+
cleanSession:
|
|
437
|
+
value.cleanSession === undefined
|
|
438
|
+
? true
|
|
439
|
+
: boolValue(value.cleanSession, 'cleanSession'),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (value.cleanSession !== undefined)
|
|
443
|
+
throw invalidOptions('cleanSession is not valid for protocolVersion 5');
|
|
444
|
+
const sessionExpiry =
|
|
445
|
+
value.sessionExpiryIntervalSeconds === undefined
|
|
446
|
+
? 0
|
|
447
|
+
: integer(
|
|
448
|
+
value.sessionExpiryIntervalSeconds,
|
|
449
|
+
'sessionExpiryIntervalSeconds',
|
|
450
|
+
0,
|
|
451
|
+
4294967295
|
|
452
|
+
);
|
|
453
|
+
const receiveMaximum =
|
|
454
|
+
value.receiveMaximum === undefined
|
|
455
|
+
? 65535
|
|
456
|
+
: integer(value.receiveMaximum, 'receiveMaximum', 1, 65535);
|
|
457
|
+
const cleanStart =
|
|
458
|
+
value.cleanStart === undefined
|
|
459
|
+
? true
|
|
460
|
+
: boolValue(value.cleanStart, 'cleanStart');
|
|
461
|
+
if (result.clientId === '' && !cleanStart)
|
|
462
|
+
throw invalidOptions('empty clientId requires MQTT5 cleanStart');
|
|
463
|
+
return {
|
|
464
|
+
...result,
|
|
465
|
+
protocolVersion: 5,
|
|
466
|
+
cleanStart,
|
|
467
|
+
sessionExpiryIntervalSeconds: sessionExpiry,
|
|
468
|
+
receiveMaximum,
|
|
469
|
+
userProperties:
|
|
470
|
+
value.userProperties === undefined
|
|
471
|
+
? undefined
|
|
472
|
+
: propertyPairs(value.userProperties, 'userProperties'),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
export function validateTopicName(topic: string): void {
|
|
476
|
+
if (
|
|
477
|
+
topic.length === 0 ||
|
|
478
|
+
topic.includes('\0') ||
|
|
479
|
+
topic.includes('+') ||
|
|
480
|
+
topic.includes('#') ||
|
|
481
|
+
!wellFormedMqttText(topic) ||
|
|
482
|
+
encodeUtf8(topic).length > 65535
|
|
483
|
+
)
|
|
484
|
+
throw invalidOptions('topic name is invalid');
|
|
485
|
+
}
|
|
486
|
+
export function validateTopicFilter(filter: string): void {
|
|
487
|
+
if (
|
|
488
|
+
filter.length === 0 ||
|
|
489
|
+
filter.includes('\0') ||
|
|
490
|
+
!wellFormedMqttText(filter) ||
|
|
491
|
+
encodeUtf8(filter).length > 65535 ||
|
|
492
|
+
filter
|
|
493
|
+
.split('/')
|
|
494
|
+
.some(
|
|
495
|
+
(part, index, parts) =>
|
|
496
|
+
(part.includes('+') && part !== '+') ||
|
|
497
|
+
(part.includes('#') && (part !== '#' || index !== parts.length - 1))
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
throw invalidOptions('topic filter is invalid');
|
|
501
|
+
if (filter.startsWith('$share/'))
|
|
502
|
+
throw invalidOptions('shared subscriptions are not supported');
|
|
503
|
+
}
|
|
504
|
+
export function normalizeSubscribeOptions(
|
|
505
|
+
input: SubscribeOptions | undefined,
|
|
506
|
+
protocolVersion: 4 | 5 = 5
|
|
507
|
+
): SubscribeOptions {
|
|
508
|
+
const value = input === undefined ? {} : object(input, 'subscribe options');
|
|
509
|
+
exactKeys(
|
|
510
|
+
value,
|
|
511
|
+
['qos', 'noLocal', 'retainAsPublished', 'retainHandling'],
|
|
512
|
+
'subscribe options'
|
|
513
|
+
);
|
|
514
|
+
if (
|
|
515
|
+
protocolVersion === 4 &&
|
|
516
|
+
(value.noLocal !== undefined ||
|
|
517
|
+
value.retainAsPublished !== undefined ||
|
|
518
|
+
value.retainHandling !== undefined)
|
|
519
|
+
)
|
|
520
|
+
throw invalidOptions(
|
|
521
|
+
'MQTT5 subscription options are not valid for protocolVersion 4'
|
|
522
|
+
);
|
|
523
|
+
return {
|
|
524
|
+
qos:
|
|
525
|
+
value.qos === undefined
|
|
526
|
+
? 1
|
|
527
|
+
: (integer(
|
|
528
|
+
value.qos,
|
|
529
|
+
'subscribe options.qos',
|
|
530
|
+
0,
|
|
531
|
+
2
|
|
532
|
+
) as SubscribeOptions['qos']),
|
|
533
|
+
noLocal:
|
|
534
|
+
value.noLocal === undefined
|
|
535
|
+
? false
|
|
536
|
+
: boolValue(value.noLocal, 'subscribe options.noLocal'),
|
|
537
|
+
retainAsPublished:
|
|
538
|
+
value.retainAsPublished === undefined
|
|
539
|
+
? false
|
|
540
|
+
: boolValue(
|
|
541
|
+
value.retainAsPublished,
|
|
542
|
+
'subscribe options.retainAsPublished'
|
|
543
|
+
),
|
|
544
|
+
retainHandling:
|
|
545
|
+
value.retainHandling === undefined
|
|
546
|
+
? 0
|
|
547
|
+
: (integer(
|
|
548
|
+
value.retainHandling,
|
|
549
|
+
'subscribe options.retainHandling',
|
|
550
|
+
0,
|
|
551
|
+
2
|
|
552
|
+
) as SubscribeOptions['retainHandling']),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
export function normalizePublishOptions(
|
|
556
|
+
input: PublishOptions | undefined,
|
|
557
|
+
protocolVersion: 4 | 5 = 5
|
|
558
|
+
): PublishOptions {
|
|
559
|
+
const value = input === undefined ? {} : object(input, 'publish options');
|
|
560
|
+
exactKeys(
|
|
561
|
+
value,
|
|
562
|
+
[
|
|
563
|
+
'qos',
|
|
564
|
+
'retained',
|
|
565
|
+
'userProperties',
|
|
566
|
+
'contentType',
|
|
567
|
+
'responseTopic',
|
|
568
|
+
'correlationDataBase64',
|
|
569
|
+
'messageExpiryIntervalSeconds',
|
|
570
|
+
'payloadFormatIndicator',
|
|
571
|
+
],
|
|
572
|
+
'publish options'
|
|
573
|
+
);
|
|
574
|
+
if (
|
|
575
|
+
protocolVersion === 4 &&
|
|
576
|
+
(value.userProperties !== undefined ||
|
|
577
|
+
value.contentType !== undefined ||
|
|
578
|
+
value.responseTopic !== undefined ||
|
|
579
|
+
value.correlationDataBase64 !== undefined ||
|
|
580
|
+
value.messageExpiryIntervalSeconds !== undefined ||
|
|
581
|
+
value.payloadFormatIndicator !== undefined)
|
|
582
|
+
)
|
|
583
|
+
throw invalidOptions(
|
|
584
|
+
'MQTT5 publish options are not valid for protocolVersion 4'
|
|
585
|
+
);
|
|
586
|
+
return {
|
|
587
|
+
qos:
|
|
588
|
+
value.qos === undefined
|
|
589
|
+
? 1
|
|
590
|
+
: (integer(
|
|
591
|
+
value.qos,
|
|
592
|
+
'publish options.qos',
|
|
593
|
+
0,
|
|
594
|
+
2
|
|
595
|
+
) as PublishOptions['qos']),
|
|
596
|
+
retained:
|
|
597
|
+
value.retained === undefined
|
|
598
|
+
? false
|
|
599
|
+
: boolValue(value.retained, 'publish options.retained'),
|
|
600
|
+
userProperties:
|
|
601
|
+
value.userProperties === undefined
|
|
602
|
+
? undefined
|
|
603
|
+
: propertyPairs(value.userProperties, 'publish options.userProperties'),
|
|
604
|
+
contentType:
|
|
605
|
+
value.contentType === undefined
|
|
606
|
+
? undefined
|
|
607
|
+
: mqttString(value.contentType, 'publish options.contentType'),
|
|
608
|
+
responseTopic:
|
|
609
|
+
value.responseTopic === undefined
|
|
610
|
+
? undefined
|
|
611
|
+
: mqttString(
|
|
612
|
+
value.responseTopic,
|
|
613
|
+
'publish options.responseTopic',
|
|
614
|
+
false
|
|
615
|
+
),
|
|
616
|
+
correlationDataBase64:
|
|
617
|
+
value.correlationDataBase64 === undefined
|
|
618
|
+
? undefined
|
|
619
|
+
: base64(
|
|
620
|
+
value.correlationDataBase64,
|
|
621
|
+
'publish options.correlationDataBase64'
|
|
622
|
+
),
|
|
623
|
+
messageExpiryIntervalSeconds:
|
|
624
|
+
value.messageExpiryIntervalSeconds === undefined
|
|
625
|
+
? undefined
|
|
626
|
+
: integer(
|
|
627
|
+
value.messageExpiryIntervalSeconds,
|
|
628
|
+
'publish options.messageExpiryIntervalSeconds',
|
|
629
|
+
0,
|
|
630
|
+
4294967295
|
|
631
|
+
),
|
|
632
|
+
payloadFormatIndicator:
|
|
633
|
+
value.payloadFormatIndicator === undefined
|
|
634
|
+
? undefined
|
|
635
|
+
: (integer(
|
|
636
|
+
value.payloadFormatIndicator,
|
|
637
|
+
'publish options.payloadFormatIndicator',
|
|
638
|
+
0,
|
|
639
|
+
1
|
|
640
|
+
) as PublishOptions['payloadFormatIndicator']),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
export function toWireSessionOptions(options: MqttOptions): WireSessionOptions {
|
|
644
|
+
const normalized = normalizeOptions(options);
|
|
645
|
+
const wire: WireSessionOptions = {
|
|
646
|
+
host: normalized.host,
|
|
647
|
+
port: normalized.port,
|
|
648
|
+
protocolVersion: normalized.protocolVersion,
|
|
649
|
+
transport: normalized.transport,
|
|
650
|
+
path: normalized.path,
|
|
651
|
+
clientId: normalized.clientId,
|
|
652
|
+
username: normalized.username,
|
|
653
|
+
password: normalized.password,
|
|
654
|
+
keepAliveSeconds: normalized.keepAliveSeconds,
|
|
655
|
+
connectTimeoutMs: normalized.connectTimeoutMs,
|
|
656
|
+
operationTimeoutMs: normalized.operationTimeoutMs,
|
|
657
|
+
disconnectTimeoutMs: normalized.disconnectTimeoutMs,
|
|
658
|
+
reconnectEnabled: normalized.reconnect?.enabled,
|
|
659
|
+
reconnectInitialDelayMs: normalized.reconnect?.initialDelayMs,
|
|
660
|
+
reconnectMaxDelayMs: normalized.reconnect?.maxDelayMs,
|
|
661
|
+
reconnectJitterRatio: normalized.reconnect?.jitterRatio,
|
|
662
|
+
tlsCaBase64: normalized.tls?.caBase64,
|
|
663
|
+
tlsP12Base64: normalized.tls?.p12Base64,
|
|
664
|
+
tlsP12Password: normalized.tls?.p12Password,
|
|
665
|
+
tlsInsecure: normalized.tls?.insecure,
|
|
666
|
+
tlsAllowInvalidHostnameForTesting:
|
|
667
|
+
normalized.tls?.allowInvalidHostnameForTesting,
|
|
668
|
+
willTopic: normalized.will?.topic,
|
|
669
|
+
willPayloadBase64: normalized.will?.payloadBase64,
|
|
670
|
+
willQos: normalized.will?.qos,
|
|
671
|
+
willRetained: normalized.will?.retained,
|
|
672
|
+
cleanSession:
|
|
673
|
+
normalized.protocolVersion === 4 ? normalized.cleanSession : undefined,
|
|
674
|
+
cleanStart:
|
|
675
|
+
normalized.protocolVersion === 5 ? normalized.cleanStart : undefined,
|
|
676
|
+
sessionExpiryIntervalSeconds:
|
|
677
|
+
normalized.protocolVersion === 5
|
|
678
|
+
? normalized.sessionExpiryIntervalSeconds
|
|
679
|
+
: undefined,
|
|
680
|
+
receiveMaximum:
|
|
681
|
+
normalized.protocolVersion === 5 ? normalized.receiveMaximum : undefined,
|
|
682
|
+
userProperties:
|
|
683
|
+
normalized.protocolVersion === 5
|
|
684
|
+
? normalized.userProperties?.map(toWireProperty)
|
|
685
|
+
: undefined,
|
|
686
|
+
maxQueuedMessages: normalized.delivery?.maxQueuedMessages,
|
|
687
|
+
maxQueuedBytes: normalized.delivery?.maxQueuedBytes,
|
|
688
|
+
maxMessageBytes: normalized.delivery?.maxMessageBytes,
|
|
689
|
+
batchMaxMessages: normalized.delivery?.batchMaxMessages,
|
|
690
|
+
batchMaxBytes: normalized.delivery?.batchMaxBytes,
|
|
691
|
+
batchAckTimeoutMs: normalized.delivery?.batchAckTimeoutMs,
|
|
692
|
+
overflowPolicy: normalized.delivery?.overflowPolicy,
|
|
693
|
+
};
|
|
694
|
+
return wire;
|
|
695
|
+
}
|
|
696
|
+
function toWireProperty(property: UserProperty): WireUserProperty {
|
|
697
|
+
return { key: property.key, value: property.value };
|
|
698
|
+
}
|
|
699
|
+
export function toWireSubscribeOptions(
|
|
700
|
+
options: SubscribeOptions | undefined
|
|
701
|
+
): WireSubscribeOptions {
|
|
702
|
+
const normalized = normalizeSubscribeOptions(options);
|
|
703
|
+
return {
|
|
704
|
+
qos: normalized.qos,
|
|
705
|
+
noLocal: normalized.noLocal,
|
|
706
|
+
retainAsPublished: normalized.retainAsPublished,
|
|
707
|
+
retainHandling: normalized.retainHandling,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
export function toWirePublishRequest(
|
|
711
|
+
topic: string,
|
|
712
|
+
payloadBase64: string,
|
|
713
|
+
options: PublishOptions | undefined,
|
|
714
|
+
protocolVersion: 4 | 5 = 5
|
|
715
|
+
): WirePublishRequest {
|
|
716
|
+
validateTopicName(topic);
|
|
717
|
+
const normalized = normalizePublishOptions(options, protocolVersion);
|
|
718
|
+
if (normalized.responseTopic !== undefined)
|
|
719
|
+
validateTopicName(normalized.responseTopic);
|
|
720
|
+
const payload = decodeBase64(payloadBase64);
|
|
721
|
+
if (normalized.payloadFormatIndicator === 1) decodeUtf8(payload);
|
|
722
|
+
return {
|
|
723
|
+
topic,
|
|
724
|
+
payloadBase64: encodeBase64(payload),
|
|
725
|
+
qos: normalized.qos,
|
|
726
|
+
retained: normalized.retained,
|
|
727
|
+
userProperties: normalized.userProperties?.map(toWireProperty),
|
|
728
|
+
contentType: normalized.contentType,
|
|
729
|
+
responseTopic: normalized.responseTopic,
|
|
730
|
+
correlationDataBase64: normalized.correlationDataBase64,
|
|
731
|
+
messageExpiryIntervalSeconds: normalized.messageExpiryIntervalSeconds,
|
|
732
|
+
payloadFormatIndicator: normalized.payloadFormatIndicator,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
export { encodeBase64 };
|