@cnhongwei/react-native-mqtt 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +169 -2
  3. package/README.zh-CN.md +100 -0
  4. package/ReactNativeMqtt.podspec +28 -0
  5. package/THIRD_PARTY_NOTICES.md +17 -0
  6. package/android/build.gradle +70 -0
  7. package/android/consumer-rules.pro +10 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttOptionsParser.kt +176 -0
  10. package/android/src/main/java/com/cnhongwei/reactnativemqtt/MqttSession.kt +1006 -0
  11. package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoLogger.java +44 -0
  12. package/android/src/main/java/com/cnhongwei/reactnativemqtt/PahoMqttTransport.kt +399 -0
  13. package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttModule.kt +619 -0
  14. package/android/src/main/java/com/cnhongwei/reactnativemqtt/ReactNativeMqttPackage.kt +31 -0
  15. package/ios/ReactNativeMqtt.h +15 -0
  16. package/ios/ReactNativeMqtt.mm +1184 -0
  17. package/ios/ReactNativeMqttCocoaTransport.swift +782 -0
  18. package/ios/ReactNativeMqttConnAckProbe.swift +87 -0
  19. package/ios/ReactNativeMqttSession.swift +420 -0
  20. package/lib/module/NativeReactNativeMqtt.js +5 -0
  21. package/lib/module/NativeReactNativeMqtt.js.map +1 -0
  22. package/lib/module/client.js +555 -0
  23. package/lib/module/client.js.map +1 -0
  24. package/lib/module/errors.js +20 -0
  25. package/lib/module/errors.js.map +1 -0
  26. package/lib/module/helpers.js +116 -0
  27. package/lib/module/helpers.js.map +1 -0
  28. package/lib/module/index.js +8 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/package.json +1 -0
  31. package/lib/module/types.js +2 -0
  32. package/lib/module/types.js.map +1 -0
  33. package/lib/module/validation.js +306 -0
  34. package/lib/module/validation.js.map +1 -0
  35. package/lib/typescript/package.json +1 -0
  36. package/lib/typescript/src/NativeReactNativeMqtt.d.ts +154 -0
  37. package/lib/typescript/src/NativeReactNativeMqtt.d.ts.map +1 -0
  38. package/lib/typescript/src/client.d.ts +3 -0
  39. package/lib/typescript/src/client.d.ts.map +1 -0
  40. package/lib/typescript/src/errors.d.ts +18 -0
  41. package/lib/typescript/src/errors.d.ts.map +1 -0
  42. package/lib/typescript/src/helpers.d.ts +5 -0
  43. package/lib/typescript/src/helpers.d.ts.map +1 -0
  44. package/lib/typescript/src/index.d.ts +9 -0
  45. package/lib/typescript/src/index.d.ts.map +1 -0
  46. package/lib/typescript/src/types.d.ts +161 -0
  47. package/lib/typescript/src/types.d.ts.map +1 -0
  48. package/lib/typescript/src/validation.d.ts +13 -0
  49. package/lib/typescript/src/validation.d.ts.map +1 -0
  50. package/package.json +188 -4
  51. package/react-native.config.js +1 -0
  52. package/src/NativeReactNativeMqtt.ts +173 -0
  53. package/src/client.ts +859 -0
  54. package/src/errors.ts +51 -0
  55. package/src/helpers.ts +151 -0
  56. package/src/index.ts +30 -0
  57. package/src/types.ts +184 -0
  58. package/src/validation.ts +735 -0
@@ -0,0 +1,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 };