@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
package/src/client.ts ADDED
@@ -0,0 +1,859 @@
1
+ import NativeReactNativeMqtt, {
2
+ type WireBatch,
3
+ type WireConnAck,
4
+ type WireDiagnostic,
5
+ type WireMessage,
6
+ type WireSnapshot,
7
+ type WireSubscriptionResult,
8
+ } from './NativeReactNativeMqtt';
9
+ import { MqttError, type MqttErrorCode } from './errors';
10
+ import { decodeBase64, encodeBase64, encodeUtf8 } from './helpers';
11
+ import type {
12
+ ConnAck,
13
+ ConnectionState,
14
+ Diagnostic,
15
+ Listener,
16
+ Message,
17
+ MqttClient,
18
+ MqttOptions,
19
+ SessionSnapshot,
20
+ SubscribeOptions,
21
+ Subscription,
22
+ } from './types';
23
+ import {
24
+ normalizeOptions,
25
+ normalizeSubscribeOptions,
26
+ toWirePublishRequest,
27
+ toWireSessionOptions,
28
+ toWireSubscribeOptions,
29
+ validateTopicFilter,
30
+ } from './validation';
31
+
32
+ type SnapshotListener = (snapshot: SessionSnapshot) => void;
33
+ type MessageListener = (message: Message) => void;
34
+ type DiagnosticListener = (diagnostic: Diagnostic) => void;
35
+ type NativeErrorShape = {
36
+ code?: string;
37
+ message?: string;
38
+ userInfo?: {
39
+ operation?: unknown;
40
+ generation?: unknown;
41
+ reasonCode?: unknown;
42
+ reasonString?: unknown;
43
+ outcome?: unknown;
44
+ } | null;
45
+ };
46
+
47
+ const errorCodes = new Set<MqttErrorCode>([
48
+ 'ERR_INVALID_OPTIONS',
49
+ 'ERR_INVALID_PAYLOAD',
50
+ 'ERR_NATIVE_UNAVAILABLE',
51
+ 'ERR_CONTRACT_MISMATCH',
52
+ 'ERR_NOT_CONNECTED',
53
+ 'ERR_DISPOSED',
54
+ 'ERR_OPERATION_IN_PROGRESS',
55
+ 'ERR_OPERATION_SUPERSEDED',
56
+ 'ERR_CONNECT_TIMEOUT',
57
+ 'ERR_OPERATION_TIMEOUT',
58
+ 'ERR_CONNECTION_REFUSED',
59
+ 'ERR_CONNECTION_LOST',
60
+ 'ERR_SUBSCRIPTION_CONFLICT',
61
+ 'ERR_SUBSCRIBE_FAILED',
62
+ 'ERR_UNSUBSCRIBE_FAILED',
63
+ 'ERR_PUBLISH_FAILED',
64
+ 'ERR_TLS',
65
+ 'ERR_CLEANUP_FAILED',
66
+ 'ERR_UNKNOWN',
67
+ ]);
68
+
69
+ function mapError(value: unknown, operation?: string): MqttError {
70
+ if (value instanceof MqttError) return value;
71
+ const shape = (
72
+ value !== null && typeof value === 'object' ? value : {}
73
+ ) as NativeErrorShape;
74
+ const code =
75
+ typeof shape.code === 'string' &&
76
+ errorCodes.has(shape.code as MqttErrorCode)
77
+ ? (shape.code as MqttErrorCode)
78
+ : 'ERR_UNKNOWN';
79
+ const message =
80
+ typeof shape.message === 'string'
81
+ ? shape.message
82
+ : `${operation ?? 'operation'} failed`;
83
+ const userInfo = shape.userInfo;
84
+ const reasonCode =
85
+ userInfo !== null &&
86
+ userInfo !== undefined &&
87
+ typeof userInfo.reasonCode === 'number'
88
+ ? userInfo.reasonCode
89
+ : undefined;
90
+ const reasonString =
91
+ userInfo !== null &&
92
+ userInfo !== undefined &&
93
+ typeof userInfo.reasonString === 'string'
94
+ ? userInfo.reasonString
95
+ : undefined;
96
+ const nativeOperation =
97
+ userInfo !== null &&
98
+ userInfo !== undefined &&
99
+ typeof userInfo.operation === 'string'
100
+ ? userInfo.operation
101
+ : undefined;
102
+ const generation =
103
+ userInfo !== null &&
104
+ userInfo !== undefined &&
105
+ typeof userInfo.generation === 'number' &&
106
+ Number.isSafeInteger(userInfo.generation)
107
+ ? userInfo.generation
108
+ : undefined;
109
+ const outcome =
110
+ userInfo !== null &&
111
+ userInfo !== undefined &&
112
+ (userInfo.outcome === 'not-sent' ||
113
+ userInfo.outcome === 'rejected' ||
114
+ userInfo.outcome === 'unknown')
115
+ ? userInfo.outcome
116
+ : 'unknown';
117
+ return new MqttError(code, message, {
118
+ operation: nativeOperation ?? operation,
119
+ generation,
120
+ reasonCode,
121
+ reasonString,
122
+ outcome,
123
+ });
124
+ }
125
+ function snapshotFromWire(value: WireSnapshot): SessionSnapshot {
126
+ return {
127
+ ...value,
128
+ state: value.state as ConnectionState,
129
+ subscriptionsStatus:
130
+ value.subscriptionsStatus as SessionSnapshot['subscriptionsStatus'],
131
+ deliveryStatus: value.deliveryStatus as SessionSnapshot['deliveryStatus'],
132
+ lastConnAck:
133
+ value.lastConnAck === null ? null : connAckFromWire(value.lastConnAck),
134
+ };
135
+ }
136
+ function connAckFromWire(value: WireConnAck): ConnAck {
137
+ return {
138
+ sessionPresent: value.sessionPresent,
139
+ assignedClientId: value.assignedClientId,
140
+ reasonCode: value.reasonCode,
141
+ reasonString: value.reasonString,
142
+ userProperties: value.userProperties?.map((pair) => ({
143
+ key: pair.key,
144
+ value: pair.value,
145
+ })),
146
+ };
147
+ }
148
+ function messageFromWire(value: WireMessage): Message {
149
+ return {
150
+ instanceId: value.instanceId,
151
+ generation: value.generation,
152
+ topic: value.topic,
153
+ bytes: decodeBase64(value.payloadBase64),
154
+ qos: value.qos as Message['qos'],
155
+ retained: value.retained,
156
+ userProperties: value.userProperties?.map((pair) => ({
157
+ key: pair.key,
158
+ value: pair.value,
159
+ })),
160
+ contentType: value.contentType,
161
+ responseTopic: value.responseTopic,
162
+ correlationData:
163
+ value.correlationDataBase64 === undefined
164
+ ? undefined
165
+ : decodeBase64(value.correlationDataBase64),
166
+ messageExpiryIntervalSeconds: value.messageExpiryIntervalSeconds,
167
+ payloadFormatIndicator:
168
+ value.payloadFormatIndicator as Message['payloadFormatIndicator'],
169
+ };
170
+ }
171
+ function diagnosticFromWire(value: WireDiagnostic): Diagnostic {
172
+ return { ...value, code: value.code as Diagnostic['code'] };
173
+ }
174
+ function randomClientId(): string {
175
+ const alphabet =
176
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
177
+ let result = 'rn';
178
+ while (result.length < 23)
179
+ result += alphabet[Math.floor(Math.random() * alphabet.length)] ?? 'x';
180
+ return result;
181
+ }
182
+
183
+ export function createMqttClient(input: MqttOptions): MqttClient {
184
+ const options = normalizeOptions(input);
185
+ const instanceId = `mqtt-${randomClientId()}`;
186
+ const clientId =
187
+ options.clientId === undefined ? randomClientId() : options.clientId;
188
+ const sessionOptions = { ...options, clientId } as MqttOptions;
189
+ let snapshot: SessionSnapshot = {
190
+ instanceId,
191
+ revision: 0,
192
+ generation: 0,
193
+ state: 'idle',
194
+ lastConnAck: null,
195
+ subscriptionsRevision: 0,
196
+ subscriptionsStatus: 'ready',
197
+ deliveryStatus: 'active',
198
+ };
199
+ let openPromise: Promise<void> | undefined;
200
+ let opened = false;
201
+ let connectPromise: Promise<ConnAck> | undefined;
202
+ let lastConnAck: ConnAck | undefined;
203
+ let disconnectPromise: Promise<void> | undefined;
204
+ let disposePromise: Promise<void> | undefined;
205
+ let nativeBound = false;
206
+ let nextIntentRevision = snapshot.subscriptionsRevision;
207
+ const stateListeners = new Set<SnapshotListener>();
208
+ const messageListeners = new Set<MessageListener>();
209
+ const diagnosticListeners = new Set<DiagnosticListener>();
210
+ const nativeListeners: Array<Listener | (() => void)> = [];
211
+ const ackTimers = new Map<string, ReturnType<typeof setTimeout>>();
212
+ const ackRetryTimers = new Map<string, ReturnType<typeof setTimeout>>();
213
+ const ackRetryAttempts = new Map<string, number>();
214
+ const subscriptions = new Map<
215
+ string,
216
+ { options: SubscribeOptions; refs: number; qos: Subscription['qos'] }
217
+ >();
218
+ const pendingSubscriptions = new Map<
219
+ string,
220
+ { options: SubscribeOptions; promise: Promise<number> }
221
+ >();
222
+ const pendingOfflineRemovals = new Map<string, number>();
223
+ const pendingIntentOperations = new Set<Promise<unknown>>();
224
+ const pendingNativeOperations = new Set<Promise<unknown>>();
225
+
226
+ const emitSnapshot = (next: SessionSnapshot): void => {
227
+ if (next.revision < snapshot.revision) return;
228
+ snapshot = next;
229
+ nextIntentRevision = Math.max(
230
+ nextIntentRevision,
231
+ next.subscriptionsRevision
232
+ );
233
+ for (const listener of [...stateListeners]) {
234
+ try {
235
+ listener(snapshot);
236
+ } catch (error) {
237
+ emitDiagnostic({
238
+ instanceId,
239
+ generation: snapshot.generation,
240
+ code: 'LISTENER_ERROR',
241
+ message: error instanceof Error ? error.message : String(error),
242
+ });
243
+ }
244
+ }
245
+ };
246
+ const emitNativeLifecycleSnapshot = (value: WireSnapshot): void => {
247
+ const next = snapshotFromWire(value);
248
+ emitSnapshot(
249
+ next.revision <= snapshot.revision
250
+ ? { ...next, revision: snapshot.revision + 1 }
251
+ : next
252
+ );
253
+ };
254
+ const emitDiagnostic = (diagnostic: Diagnostic): void => {
255
+ for (const listener of [...diagnosticListeners]) {
256
+ try {
257
+ listener(diagnostic);
258
+ } catch {
259
+ /* diagnostic listeners are isolated */
260
+ }
261
+ }
262
+ };
263
+ const batchKey = (streamEpoch: number, batchId: number): string =>
264
+ `${streamEpoch}:${batchId}`;
265
+ const allocateIntentRevision = (): number => {
266
+ nextIntentRevision =
267
+ Math.max(nextIntentRevision, snapshot.subscriptionsRevision) + 1;
268
+ return nextIntentRevision;
269
+ };
270
+ const trackIntentOperation = <T>(promise: Promise<T>): Promise<T> => {
271
+ pendingIntentOperations.add(promise);
272
+ promise.then(
273
+ () => pendingIntentOperations.delete(promise),
274
+ () => pendingIntentOperations.delete(promise)
275
+ );
276
+ return promise;
277
+ };
278
+ const trackNativeOperation = <T>(promise: Promise<T>): Promise<T> => {
279
+ pendingNativeOperations.add(promise);
280
+ promise.then(
281
+ () => pendingNativeOperations.delete(promise),
282
+ () => pendingNativeOperations.delete(promise)
283
+ );
284
+ return promise;
285
+ };
286
+ const flushOfflineRemovals = (): void => {
287
+ if (snapshot.state !== 'connected' || pendingOfflineRemovals.size === 0)
288
+ return;
289
+ const native = bindNative();
290
+ for (const [topicFilter, intentRevision] of [
291
+ ...pendingOfflineRemovals.entries(),
292
+ ]) {
293
+ pendingOfflineRemovals.delete(topicFilter);
294
+ const operation = trackIntentOperation(
295
+ native
296
+ .removeSubscription(instanceId, topicFilter, intentRevision)
297
+ .catch((error: unknown) => {
298
+ pendingOfflineRemovals.set(topicFilter, intentRevision);
299
+ throw error;
300
+ })
301
+ );
302
+ operation.catch(() => undefined);
303
+ }
304
+ };
305
+ const acknowledgeBatch = (
306
+ native: NonNullable<typeof NativeReactNativeMqtt>,
307
+ streamEpoch: number,
308
+ batchId: number
309
+ ): void => {
310
+ const key = batchKey(streamEpoch, batchId);
311
+ const previous = ackTimers.get(key);
312
+ if (previous !== undefined) clearTimeout(previous);
313
+ const timeout = setTimeout(() => {
314
+ if (!ackTimers.has(key) || snapshot.state === 'closed') return;
315
+ emitSnapshot({
316
+ ...snapshot,
317
+ deliveryStatus: 'stalled',
318
+ revision: snapshot.revision + 1,
319
+ });
320
+ emitDiagnostic({
321
+ instanceId,
322
+ generation: snapshot.generation,
323
+ code: 'DELIVERY_STALLED',
324
+ message: `Message batch ${key} acknowledgement timed out`,
325
+ });
326
+ }, options.delivery?.batchAckTimeoutMs ?? 15000);
327
+ // Install the watchdog before submitting: a synchronously-resolved native
328
+ // promise must still be able to clear the exact timer it owns.
329
+ ackTimers.set(key, timeout);
330
+ const submit = (attempt: number): void => {
331
+ native
332
+ .ackMessageBatch(instanceId, streamEpoch, batchId)
333
+ .then(() => {
334
+ ackTimers.delete(key);
335
+ ackRetryAttempts.delete(key);
336
+ ackRetryTimers.delete(key);
337
+ if (snapshot.deliveryStatus === 'stalled')
338
+ emitSnapshot({
339
+ ...snapshot,
340
+ deliveryStatus: 'active',
341
+ revision: snapshot.revision + 1,
342
+ });
343
+ })
344
+ .catch((error: unknown) => {
345
+ if (attempt === 0 && snapshot.state !== 'closed') {
346
+ ackRetryAttempts.set(key, 1);
347
+ const retry = setTimeout(() => {
348
+ ackRetryTimers.delete(key);
349
+ if (snapshot.state !== 'closed') submit(1);
350
+ }, 0);
351
+ ackRetryTimers.set(key, retry);
352
+ return;
353
+ }
354
+ emitDiagnostic({
355
+ instanceId,
356
+ generation: snapshot.generation,
357
+ code: 'DELIVERY_STALLED',
358
+ message: mapError(error, 'ackMessageBatch').message,
359
+ });
360
+ });
361
+ };
362
+ submit(ackRetryAttempts.get(key) ?? 0);
363
+ };
364
+ const nativeOrThrow = (): NonNullable<typeof NativeReactNativeMqtt> => {
365
+ if (NativeReactNativeMqtt == null)
366
+ throw new MqttError(
367
+ 'ERR_NATIVE_UNAVAILABLE',
368
+ 'ReactNativeMqtt TurboModule is unavailable; rebuild the app with the native module installed',
369
+ { outcome: 'not-sent' }
370
+ );
371
+ return NativeReactNativeMqtt;
372
+ };
373
+ const bindNative = (): NonNullable<typeof NativeReactNativeMqtt> => {
374
+ const native = nativeOrThrow();
375
+ if (!nativeBound) {
376
+ nativeBound = true;
377
+ nativeListeners.push(
378
+ native.onSessionSnapshot((value) => {
379
+ if (snapshot.state === 'closed' || value.instanceId !== instanceId)
380
+ return;
381
+ emitSnapshot(snapshotFromWire(value));
382
+ })
383
+ );
384
+ nativeListeners.push(
385
+ native.onMessageBatch((batch: WireBatch) => {
386
+ if (snapshot.state === 'closed' || batch.instanceId !== instanceId)
387
+ return;
388
+ // Keep the native in-flight token reserved while stalled. A late or
389
+ // duplicate event must not deliver another batch or release capacity
390
+ // until the original token is acknowledged.
391
+ if (snapshot.deliveryStatus === 'stalled') return;
392
+ try {
393
+ for (const value of batch.messages) {
394
+ if (value.instanceId !== instanceId) continue;
395
+ let message: Message;
396
+ try {
397
+ message = messageFromWire(value);
398
+ } catch (error) {
399
+ emitDiagnostic({
400
+ instanceId,
401
+ generation: snapshot.generation,
402
+ code: 'LISTENER_ERROR',
403
+ message:
404
+ error instanceof Error ? error.message : String(error),
405
+ });
406
+ continue;
407
+ }
408
+ for (const listener of [...messageListeners]) {
409
+ try {
410
+ const result = (listener as (value: Message) => unknown)(
411
+ message
412
+ );
413
+ if (
414
+ result !== null &&
415
+ (typeof result === 'object' || typeof result === 'function')
416
+ ) {
417
+ Promise.resolve(result as PromiseLike<unknown>).catch(
418
+ (error: unknown) =>
419
+ emitDiagnostic({
420
+ instanceId,
421
+ generation: snapshot.generation,
422
+ code: 'LISTENER_ERROR',
423
+ message:
424
+ error instanceof Error
425
+ ? error.message
426
+ : String(error),
427
+ })
428
+ );
429
+ }
430
+ } catch (error) {
431
+ emitDiagnostic({
432
+ instanceId,
433
+ generation: snapshot.generation,
434
+ code: 'LISTENER_ERROR',
435
+ message:
436
+ error instanceof Error ? error.message : String(error),
437
+ });
438
+ }
439
+ }
440
+ }
441
+ } finally {
442
+ acknowledgeBatch(native, batch.streamEpoch, batch.batchId);
443
+ }
444
+ })
445
+ );
446
+ nativeListeners.push(
447
+ native.onDiagnostic((value) => {
448
+ if (snapshot.state === 'closed' || value.instanceId !== instanceId)
449
+ return;
450
+ emitDiagnostic(diagnosticFromWire(value));
451
+ })
452
+ );
453
+ }
454
+ return native;
455
+ };
456
+ const ensureOpen = (): Promise<void> => {
457
+ if (disposePromise !== undefined)
458
+ return Promise.reject(
459
+ new MqttError('ERR_DISPOSED', 'Client is disposed', {
460
+ outcome: 'not-sent',
461
+ })
462
+ );
463
+ if (openPromise !== undefined) return openPromise;
464
+ openPromise = Promise.resolve()
465
+ .then(() =>
466
+ bindNative().openSession(
467
+ instanceId,
468
+ toWireSessionOptions(sessionOptions)
469
+ )
470
+ )
471
+ .then((value) => {
472
+ opened = true;
473
+ emitNativeLifecycleSnapshot(value);
474
+ })
475
+ .catch((error: unknown) => {
476
+ opened = false;
477
+ openPromise = undefined;
478
+ throw mapError(error, 'openSession');
479
+ });
480
+ return openPromise;
481
+ };
482
+ const listener = <T>(set: Set<T>, value: T): Listener => {
483
+ if (disposePromise !== undefined || snapshot.state === 'closed')
484
+ return { remove: () => undefined };
485
+ set.add(value);
486
+ return { remove: () => set.delete(value) };
487
+ };
488
+ const ensureUsable = (): void => {
489
+ if (disposePromise !== undefined || snapshot.state === 'closed')
490
+ throw new MqttError('ERR_DISPOSED', 'Client is disposed', {
491
+ outcome: 'not-sent',
492
+ });
493
+ };
494
+
495
+ const client: MqttClient = {
496
+ get instanceId() {
497
+ return instanceId;
498
+ },
499
+ get clientId() {
500
+ return clientId;
501
+ },
502
+ get state() {
503
+ return snapshot.state;
504
+ },
505
+ connect: () => {
506
+ ensureUsable();
507
+ if (snapshot.state === 'disconnecting')
508
+ return Promise.reject(
509
+ new MqttError(
510
+ 'ERR_OPERATION_IN_PROGRESS',
511
+ 'Disconnect is still in progress',
512
+ { outcome: 'not-sent' }
513
+ )
514
+ );
515
+ if (snapshot.state === 'connected' && lastConnAck !== undefined)
516
+ return Promise.resolve(lastConnAck);
517
+ if (connectPromise !== undefined) return connectPromise;
518
+ connectPromise = ensureOpen()
519
+ .then(() => bindNative().connect(instanceId))
520
+ .then((value) => {
521
+ emitNativeLifecycleSnapshot(value.snapshot);
522
+ flushOfflineRemovals();
523
+ lastConnAck = connAckFromWire(value.ack);
524
+ return lastConnAck;
525
+ })
526
+ .catch((error: unknown) => {
527
+ connectPromise = undefined;
528
+ throw mapError(error, 'connect');
529
+ });
530
+ return connectPromise;
531
+ },
532
+ disconnect: () => {
533
+ if (snapshot.state === 'closed') return Promise.resolve();
534
+ ensureUsable();
535
+ if (disconnectPromise !== undefined) return disconnectPromise;
536
+ if (snapshot.state === 'idle' || snapshot.state === 'disconnected') {
537
+ // A native disconnect callback may have settled the lifecycle before
538
+ // this call reaches JS. Do not retain the resolved connect promise or
539
+ // a later explicit connect would incorrectly return the old ACK.
540
+ connectPromise = undefined;
541
+ return Promise.resolve();
542
+ }
543
+ emitSnapshot({
544
+ ...snapshot,
545
+ state: 'disconnecting',
546
+ revision: snapshot.revision + 1,
547
+ });
548
+ disconnectPromise = trackNativeOperation(
549
+ ensureOpen()
550
+ .then(() => bindNative().disconnect(instanceId))
551
+ .then((value) => {
552
+ emitNativeLifecycleSnapshot(value);
553
+ })
554
+ .catch((error: unknown) => {
555
+ throw mapError(error, 'disconnect');
556
+ })
557
+ .finally(() => {
558
+ disconnectPromise = undefined;
559
+ connectPromise = undefined;
560
+ })
561
+ );
562
+ return disconnectPromise;
563
+ },
564
+ dispose: () => {
565
+ if (disposePromise !== undefined) return disposePromise;
566
+ if (snapshot.state === 'closed') return Promise.resolve();
567
+ const pendingOperations = [...pendingIntentOperations];
568
+ const pendingNativeOperationsAtDispose = [...pendingNativeOperations];
569
+ disposePromise = (openPromise ?? Promise.resolve())
570
+ .catch(() => undefined)
571
+ .then(() =>
572
+ Promise.all(
573
+ [...pendingOperations, ...pendingNativeOperationsAtDispose].map(
574
+ (operation) => operation.catch(() => undefined)
575
+ )
576
+ )
577
+ )
578
+ .then(() =>
579
+ NativeReactNativeMqtt == null
580
+ ? undefined
581
+ : NativeReactNativeMqtt.close(instanceId)
582
+ )
583
+ .then((value) => {
584
+ if (value !== undefined) emitNativeLifecycleSnapshot(value);
585
+ else
586
+ emitSnapshot({
587
+ ...snapshot,
588
+ state: 'closed',
589
+ deliveryStatus: 'closed',
590
+ revision: snapshot.revision + 1,
591
+ });
592
+ })
593
+ .catch((error: unknown) => {
594
+ throw mapError(error, 'close');
595
+ })
596
+ .finally(() => {
597
+ for (const timer of ackTimers.values()) clearTimeout(timer);
598
+ ackTimers.clear();
599
+ for (const timer of ackRetryTimers.values()) clearTimeout(timer);
600
+ ackRetryTimers.clear();
601
+ ackRetryAttempts.clear();
602
+ for (const subscription of nativeListeners) {
603
+ if (typeof subscription === 'function') subscription();
604
+ else subscription.remove();
605
+ }
606
+ nativeListeners.length = 0;
607
+ stateListeners.clear();
608
+ messageListeners.clear();
609
+ diagnosticListeners.clear();
610
+ });
611
+ return disposePromise;
612
+ },
613
+ ready: () => {
614
+ ensureUsable();
615
+ if (snapshot.state !== 'connected')
616
+ return Promise.reject(
617
+ new MqttError('ERR_NOT_CONNECTED', 'Client is not connected', {
618
+ outcome: 'not-sent',
619
+ })
620
+ );
621
+ const targetGeneration = snapshot.generation;
622
+ const targetRevision = Math.max(
623
+ nextIntentRevision,
624
+ snapshot.subscriptionsRevision
625
+ );
626
+ const operations = [...pendingIntentOperations];
627
+ return ensureOpen()
628
+ .then(() => Promise.all(operations))
629
+ .then(() =>
630
+ bindNative().awaitSubscriptions(
631
+ instanceId,
632
+ targetGeneration,
633
+ targetRevision
634
+ )
635
+ )
636
+ .then((value) => {
637
+ emitSnapshot(snapshotFromWire(value));
638
+ });
639
+ },
640
+ getSnapshot: () => {
641
+ if (snapshot.state === 'closed' || (!opened && openPromise === undefined))
642
+ return Promise.resolve(snapshot);
643
+ return ensureOpen()
644
+ .then(() => bindNative().getSnapshot(instanceId))
645
+ .then((value) => {
646
+ const next = snapshotFromWire(value);
647
+ emitSnapshot(next);
648
+ return next;
649
+ })
650
+ .catch((error: unknown) => {
651
+ throw mapError(error, 'getSnapshot');
652
+ });
653
+ },
654
+ subscribe: (topicFilter, inputOptions) => {
655
+ ensureUsable();
656
+ validateTopicFilter(topicFilter);
657
+ pendingOfflineRemovals.delete(topicFilter);
658
+ const subscribeOptions = normalizeSubscribeOptions(
659
+ inputOptions,
660
+ options.protocolVersion ?? 4
661
+ );
662
+ const existing = subscriptions.get(topicFilter);
663
+ const createHandle = (record: {
664
+ options: SubscribeOptions;
665
+ refs: number;
666
+ qos: Subscription['qos'];
667
+ }): Subscription => {
668
+ record.refs += 1;
669
+ let released = false;
670
+ let releasePromise: Promise<void> | undefined;
671
+ return {
672
+ topicFilter,
673
+ get qos() {
674
+ return record.qos;
675
+ },
676
+ unsubscribe: async () => {
677
+ if (released) return releasePromise;
678
+ released = true;
679
+ if (record.refs > 0) record.refs -= 1;
680
+ if (record.refs !== 0) return;
681
+ subscriptions.delete(topicFilter);
682
+ const intentRevision = allocateIntentRevision();
683
+ if (snapshot.state !== 'connected') {
684
+ const removal = bindNative()
685
+ .removeSubscription(instanceId, topicFilter, intentRevision)
686
+ .then(() => undefined)
687
+ .catch((error: unknown) => {
688
+ pendingOfflineRemovals.set(topicFilter, intentRevision);
689
+ released = false;
690
+ record.refs = 1;
691
+ subscriptions.set(topicFilter, record);
692
+ throw error;
693
+ });
694
+ releasePromise = trackIntentOperation(removal).catch(
695
+ (error: unknown) => {
696
+ throw mapError(error, 'unsubscribe');
697
+ }
698
+ );
699
+ return releasePromise;
700
+ }
701
+ const removal = bindNative()
702
+ .removeSubscription(instanceId, topicFilter, intentRevision)
703
+ .then(() => undefined)
704
+ .catch((error: unknown) => {
705
+ released = false;
706
+ record.refs = 1;
707
+ subscriptions.set(topicFilter, record);
708
+ throw error;
709
+ });
710
+ releasePromise = trackIntentOperation(removal).catch(
711
+ (error: unknown) => {
712
+ throw mapError(error, 'unsubscribe');
713
+ }
714
+ );
715
+ return releasePromise;
716
+ },
717
+ };
718
+ };
719
+ if (existing !== undefined) {
720
+ if (
721
+ JSON.stringify(existing.options) !== JSON.stringify(subscribeOptions)
722
+ )
723
+ return Promise.reject(
724
+ new MqttError(
725
+ 'ERR_SUBSCRIPTION_CONFLICT',
726
+ 'Subscription options conflict for this filter',
727
+ { outcome: 'not-sent' }
728
+ )
729
+ );
730
+ return Promise.resolve(createHandle(existing));
731
+ }
732
+ const pending = pendingSubscriptions.get(topicFilter);
733
+ if (pending !== undefined) {
734
+ if (
735
+ JSON.stringify(pending.options) !== JSON.stringify(subscribeOptions)
736
+ )
737
+ return Promise.reject(
738
+ new MqttError(
739
+ 'ERR_SUBSCRIPTION_CONFLICT',
740
+ 'Subscription options conflict for this filter',
741
+ { outcome: 'not-sent' }
742
+ )
743
+ );
744
+ return pending.promise.then(() => {
745
+ const record = subscriptions.get(topicFilter);
746
+ if (record === undefined)
747
+ throw new MqttError('ERR_UNKNOWN', 'Subscription request lost', {
748
+ outcome: 'unknown',
749
+ });
750
+ return createHandle(record);
751
+ });
752
+ }
753
+ const intentRevision = allocateIntentRevision();
754
+ const promise = trackIntentOperation(
755
+ ensureOpen()
756
+ .then(() =>
757
+ bindNative().setSubscription(
758
+ instanceId,
759
+ topicFilter,
760
+ toWireSubscribeOptions(subscribeOptions),
761
+ intentRevision
762
+ )
763
+ )
764
+ .then((result: WireSubscriptionResult) => {
765
+ if (disposePromise !== undefined)
766
+ throw new MqttError('ERR_DISPOSED', 'Client is disposed', {
767
+ outcome: 'not-sent',
768
+ });
769
+ nextIntentRevision = Math.max(
770
+ nextIntentRevision,
771
+ result.intentRevision
772
+ );
773
+ const record = {
774
+ options: subscribeOptions,
775
+ refs: 0,
776
+ qos: result.grantedQos as Subscription['qos'],
777
+ };
778
+ subscriptions.set(topicFilter, record);
779
+ return result.grantedQos;
780
+ })
781
+ );
782
+ pendingSubscriptions.set(topicFilter, {
783
+ options: subscribeOptions,
784
+ promise,
785
+ });
786
+ return promise
787
+ .then(() => {
788
+ const record = subscriptions.get(topicFilter);
789
+ if (record === undefined)
790
+ throw new MqttError('ERR_UNKNOWN', 'Subscription request lost', {
791
+ outcome: 'unknown',
792
+ });
793
+ return createHandle(record);
794
+ })
795
+ .catch((error: unknown) => {
796
+ throw mapError(error, 'subscribe');
797
+ })
798
+ .finally(() => {
799
+ if (pendingSubscriptions.get(topicFilter)?.promise === promise)
800
+ pendingSubscriptions.delete(topicFilter);
801
+ });
802
+ },
803
+ publishText: (topic, text, publishOptions) => {
804
+ ensureUsable();
805
+ const payload = encodeUtf8(text);
806
+ return client.publishBytes(topic, payload, publishOptions);
807
+ },
808
+ publishBytes: (topic, bytes, publishOptions) => {
809
+ ensureUsable();
810
+ const copy = new Uint8Array(bytes);
811
+ return trackNativeOperation(
812
+ ensureOpen()
813
+ .then(() =>
814
+ bindNative().publish(
815
+ instanceId,
816
+ toWirePublishRequest(
817
+ topic,
818
+ encodeBase64(copy),
819
+ publishOptions,
820
+ options.protocolVersion ?? 4
821
+ )
822
+ )
823
+ )
824
+ .catch((error: unknown) => {
825
+ throw mapError(error, 'publish');
826
+ })
827
+ );
828
+ },
829
+ publishBase64: (topic, base64, publishOptions) => {
830
+ ensureUsable();
831
+ return trackNativeOperation(
832
+ ensureOpen()
833
+ .then(() =>
834
+ bindNative().publish(
835
+ instanceId,
836
+ toWirePublishRequest(
837
+ topic,
838
+ base64,
839
+ publishOptions,
840
+ options.protocolVersion ?? 4
841
+ )
842
+ )
843
+ )
844
+ .catch((error: unknown) => {
845
+ throw mapError(error, 'publish');
846
+ })
847
+ );
848
+ },
849
+ onStateChange: (value) => listener(stateListeners, value),
850
+ onMessage: (value) => listener(messageListeners, value),
851
+ onDiagnostic: (value) => listener(diagnosticListeners, value),
852
+ };
853
+ if (Symbol.asyncDispose !== undefined)
854
+ Object.defineProperty(client, Symbol.asyncDispose, {
855
+ value: () => client.dispose(),
856
+ enumerable: false,
857
+ });
858
+ return client;
859
+ }