react-native-ble-nitro 1.10.3 → 1.12.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 (39) hide show
  1. package/android/src/main/cpp/cpp-adapter.cpp +4 -1
  2. package/android/src/main/java/com/margelo/nitro/co/zyke/ble/BleNitroBleManager.kt +58 -21
  3. package/ios/BleNitroBleManager.swift +60 -3
  4. package/ios/BlePeripheralDelegate.swift +68 -9
  5. package/lib/commonjs/index.d.ts +1 -1
  6. package/lib/commonjs/index.d.ts.map +1 -1
  7. package/lib/commonjs/index.js +2 -1
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/manager.d.ts +19 -4
  10. package/lib/commonjs/manager.d.ts.map +1 -1
  11. package/lib/commonjs/manager.js +68 -33
  12. package/lib/commonjs/manager.js.map +1 -1
  13. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts +3 -0
  14. package/lib/commonjs/specs/NativeBleNitro.nitro.d.ts.map +1 -1
  15. package/lib/index.d.ts +1 -1
  16. package/lib/index.js +1 -1
  17. package/lib/manager.d.ts +19 -4
  18. package/lib/manager.js +66 -32
  19. package/lib/specs/NativeBleNitro.nitro.d.ts +3 -0
  20. package/nitrogen/generated/android/BleNitroOnLoad.cpp +48 -32
  21. package/nitrogen/generated/android/BleNitroOnLoad.hpp +13 -4
  22. package/nitrogen/generated/android/c++/JHybridNativeBleNitroFactorySpec.cpp +20 -26
  23. package/nitrogen/generated/android/c++/JHybridNativeBleNitroFactorySpec.hpp +19 -22
  24. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.cpp +52 -49
  25. package/nitrogen/generated/android/c++/JHybridNativeBleNitroSpec.hpp +21 -22
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroFactorySpec.kt +15 -18
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/HybridNativeBleNitroSpec.kt +28 -18
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/Variant_NullType_BLEDevice.kt +0 -6
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/co/zyke/ble/Variant_NullType_String.kt +0 -6
  30. package/nitrogen/generated/ios/c++/HybridNativeBleNitroSpecSwift.hpp +14 -0
  31. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec.swift +2 -0
  32. package/nitrogen/generated/ios/swift/HybridNativeBleNitroSpec_cxx.swift +28 -0
  33. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.cpp +2 -0
  34. package/nitrogen/generated/shared/c++/HybridNativeBleNitroSpec.hpp +2 -0
  35. package/package.json +9 -9
  36. package/src/__tests__/index.test.ts +145 -1
  37. package/src/index.ts +1 -0
  38. package/src/manager.ts +96 -34
  39. package/src/specs/NativeBleNitro.nitro.ts +3 -0
package/src/manager.ts CHANGED
@@ -7,6 +7,25 @@ import {
7
7
  AndroidScanMode as NativeAndroidScanMode,
8
8
  } from './specs/NativeBleNitro';
9
9
 
10
+ export class BleTimeoutError extends Error {
11
+ constructor(operation: string, ms: number) {
12
+ super(`${operation} timed out after ${ms}ms`);
13
+ this.name = 'BleTimeoutError';
14
+ }
15
+ }
16
+
17
+ function withTimeout<T>(
18
+ promise: Promise<T>,
19
+ ms: number,
20
+ operation: string
21
+ ): Promise<T> {
22
+ let timer: ReturnType<typeof setTimeout>;
23
+ const timeout = new Promise<never>((_, reject) => {
24
+ timer = setTimeout(() => reject(new BleTimeoutError(operation, ms)), ms);
25
+ });
26
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
27
+ }
28
+
10
29
  export type ByteArray = number[];
11
30
 
12
31
  export interface ScanFilter {
@@ -127,7 +146,6 @@ export function byteArrayToArrayBuffer(data: ByteArray): ArrayBuffer {
127
146
 
128
147
  export class BleNitroManager {
129
148
  private _isScanning: boolean = false;
130
- private _connectedDevices: { [deviceId: string]: boolean } = {};
131
149
 
132
150
  private _restoredStateCallback: RestoreStateCallback | null;
133
151
  private _restoredState: BLEDevice[] | null = null;
@@ -144,9 +162,6 @@ export class BleNitroManager {
144
162
  private onNativeRestoreStateCallback(peripherals: NativeBLEDevice[]) {
145
163
  if (!this._restoreStateIdentifier) return;
146
164
  const bleDevices = peripherals.map((peripheral) => convertNativeBleDeviceToBleDevice(peripheral));
147
- bleDevices.forEach((device) => {
148
- this._connectedDevices[device.id] = device.isConnected;
149
- });
150
165
  if (this._restoredStateCallback) {
151
166
  this._restoredStateCallback(bleDevices);
152
167
  } else {
@@ -289,7 +304,7 @@ export class BleNitroManager {
289
304
  ): Promise<string> {
290
305
  return new Promise((resolve, reject) => {
291
306
  // Check if already connected
292
- if (this._connectedDevices[deviceId]) {
307
+ if (this.isConnected(deviceId)) {
293
308
  resolve(deviceId);
294
309
  return;
295
310
  }
@@ -298,15 +313,12 @@ export class BleNitroManager {
298
313
  deviceId,
299
314
  (success: boolean, connectedDeviceId: string, error: string) => {
300
315
  if (success) {
301
- this._connectedDevices[deviceId] = true;
302
316
  resolve(connectedDeviceId);
303
317
  } else {
304
318
  reject(new Error(error));
305
319
  }
306
320
  },
307
321
  onDisconnect ? (deviceId: string, interrupted: boolean, error: string) => {
308
- // Remove from connected devices when disconnected
309
- delete this._connectedDevices[deviceId];
310
322
  onDisconnect(deviceId, interrupted, error);
311
323
  } : undefined,
312
324
  autoConnectAndroid ?? false,
@@ -321,8 +333,7 @@ export class BleNitroManager {
321
333
  * @returns Promise resolving deviceId when connected
322
334
  */
323
335
  public findAndConnect(deviceId: string, options?: { scanTimeout?: number, autoConnectAndroid?: boolean, onDisconnect?: DisconnectEventCallback, onFound?: (device: BLEDevice) => void }): Promise<string> {
324
- const isConnected = this.isConnected(deviceId);
325
- if (isConnected) {
336
+ if (this.isConnected(deviceId)) {
326
337
  return Promise.resolve(deviceId);
327
338
  }
328
339
  if (this._isScanning) {
@@ -351,14 +362,13 @@ export class BleNitroManager {
351
362
  /**
352
363
  * Disconnect from a Bluetooth device
353
364
  * @param deviceId ID of the device to disconnect from
354
- * @returns Promise resolving when disconnected
365
+ * @returns Promise resolving with devices uuid or mac address when disconnected
355
366
  */
356
- public disconnect(deviceId: string): Promise<void> {
367
+ public disconnect(deviceId: string): Promise<string> {
357
368
  return new Promise((resolve, reject) => {
358
369
  // Check if already disconnected
359
- if (!this._connectedDevices[deviceId] || !this.isConnected(deviceId)) {
360
- delete this._connectedDevices[deviceId];
361
- resolve();
370
+ if (!this.isConnected(deviceId)) {
371
+ resolve(deviceId);
362
372
  return;
363
373
  }
364
374
 
@@ -366,8 +376,7 @@ export class BleNitroManager {
366
376
  deviceId,
367
377
  (success: boolean, error: string) => {
368
378
  if (success) {
369
- delete this._connectedDevices[deviceId];
370
- resolve();
379
+ resolve(deviceId);
371
380
  } else {
372
381
  reject(new Error(error));
373
382
  }
@@ -405,7 +414,7 @@ export class BleNitroManager {
405
414
  public readRSSI(deviceId: string): Promise<number> {
406
415
  return new Promise((resolve, reject) => {
407
416
  // Check if connected first
408
- if (!this._connectedDevices[deviceId]) {
417
+ if (!this.isConnected(deviceId)) {
409
418
  reject(new Error('Device not connected'));
410
419
  return;
411
420
  }
@@ -431,7 +440,7 @@ export class BleNitroManager {
431
440
  public discoverServices(deviceId: string): Promise<boolean> {
432
441
  return new Promise((resolve, reject) => {
433
442
  // Check if connected first
434
- if (!this._connectedDevices[deviceId]) {
443
+ if (!this.isConnected(deviceId)) {
435
444
  reject(new Error('Device not connected'));
436
445
  return;
437
446
  }
@@ -457,7 +466,7 @@ export class BleNitroManager {
457
466
  public getServices(deviceId: string): Promise<string[]> {
458
467
  return new Promise(async (resolve, reject) => {
459
468
  // Check if connected first
460
- if (!this._connectedDevices[deviceId]) {
469
+ if (!this.isConnected(deviceId)) {
461
470
  reject(new Error('Device not connected'));
462
471
  return;
463
472
  }
@@ -482,7 +491,7 @@ export class BleNitroManager {
482
491
  deviceId: string,
483
492
  serviceId: string
484
493
  ): string[] {
485
- if (!this._connectedDevices[deviceId]) {
494
+ if (!this.isConnected(deviceId)) {
486
495
  throw new Error('Device not connected');
487
496
  }
488
497
 
@@ -494,21 +503,53 @@ export class BleNitroManager {
494
503
  }
495
504
 
496
505
  /**
497
- * Get services and characteristics for a connected device
506
+ * Get services and characteristics for a connected device.
507
+ * Uses a native method that waits for both service and characteristic
508
+ * discovery to complete before reading, avoiding the CoreBluetooth race
509
+ * where didDiscoverServices may not re-fire for cached services.
498
510
  * @param deviceId ID of the device
499
511
  * @returns Promise resolving to array of service and characteristic UUIDs
500
512
  * @see getServices
501
513
  * @see getCharacteristics
502
514
  */
503
- public async getServicesWithCharacteristics(deviceId: string): Promise<{ uuid: string; characteristics: string[] }[]> {
504
- await this.discoverServices(deviceId);
505
- const services = await this.getServices(deviceId);
506
- return services.map((service) => {
507
- return {
508
- uuid: service,
509
- characteristics: this.getCharacteristics(deviceId, service),
510
- };
515
+ public async getServicesWithCharacteristics(
516
+ deviceId: string
517
+ ): Promise<{ uuid: string; characteristics: string[] }[]> {
518
+ await this._discoverServicesWithCharacteristics(deviceId);
519
+
520
+ const services = this.Instance.getServices(deviceId);
521
+ return BleNitroManager.normalizeGattUUIDs(services).map((service) => ({
522
+ uuid: service,
523
+ characteristics: this.getCharacteristics(deviceId, service),
524
+ }));
525
+ }
526
+
527
+ private static readonly DISCOVERY_TIMEOUT_MS = 30_000;
528
+
529
+ private _discoverServicesWithCharacteristics(
530
+ deviceId: string
531
+ ): Promise<void> {
532
+ const inner = new Promise<void>((resolve, reject) => {
533
+ if (!this.isConnected(deviceId)) {
534
+ reject(new Error('Device not connected'));
535
+ return;
536
+ }
537
+ this.Instance.discoverServicesWithCharacteristics(
538
+ deviceId,
539
+ (success: boolean, error: string) => {
540
+ if (success) {
541
+ resolve();
542
+ } else {
543
+ reject(new Error(error));
544
+ }
545
+ }
546
+ );
511
547
  });
548
+ return withTimeout(
549
+ inner,
550
+ BleNitroManager.DISCOVERY_TIMEOUT_MS,
551
+ 'discoverServicesWithCharacteristics'
552
+ );
512
553
  }
513
554
 
514
555
  /**
@@ -525,7 +566,7 @@ export class BleNitroManager {
525
566
  ): Promise<ByteArray> {
526
567
  return new Promise((resolve, reject) => {
527
568
  // Check if connected first
528
- if (!this._connectedDevices[deviceId]) {
569
+ if (!this.isConnected(deviceId)) {
529
570
  reject(new Error('Device not connected'));
530
571
  return;
531
572
  }
@@ -563,7 +604,7 @@ export class BleNitroManager {
563
604
  ): Promise<ByteArray> {
564
605
  return new Promise((resolve, reject) => {
565
606
  // Check if connected first
566
- if (!this._connectedDevices[deviceId]) {
607
+ if (!this.isConnected(deviceId)) {
567
608
  reject(new Error('Device not connected'));
568
609
  return;
569
610
  }
@@ -603,7 +644,7 @@ export class BleNitroManager {
603
644
  ): Promise<AsyncSubscription> {
604
645
  return new Promise((resolve, reject) => {
605
646
  // Check if connected first
606
- if (!this._connectedDevices[deviceId]) {
647
+ if (!this.isConnected(deviceId)) {
607
648
  reject(new Error('Device not connected'));
608
649
  return;
609
650
  }
@@ -651,7 +692,7 @@ export class BleNitroManager {
651
692
  ): Promise<void> {
652
693
  return new Promise((resolve, reject) => {
653
694
  // Check if connected first
654
- if (!this._connectedDevices[deviceId]) {
695
+ if (!this.isConnected(deviceId)) {
655
696
  reject(new Error('Device not connected'));
656
697
  return;
657
698
  }
@@ -671,6 +712,27 @@ export class BleNitroManager {
671
712
  });
672
713
  }
673
714
 
715
+ /**
716
+ * Check if currently subscribed to a characteristic's notifications
717
+ * @param deviceId ID of the device
718
+ * @param serviceId ID of the service
719
+ * @param characteristicId ID of the characteristic
720
+ * @returns Boolean indicating if subscribed to notifications
721
+ */
722
+ public isSubscribedToCharacteristic(
723
+ deviceId: string,
724
+ serviceId: string,
725
+ characteristicId: string
726
+ ): boolean {
727
+ // No isConnected guard — both native implementations already return false
728
+ // for disconnected devices, and an extra check would introduce a TOCTOU race.
729
+ return this.Instance.isSubscribedToCharacteristic(
730
+ deviceId,
731
+ BleNitroManager.normalizeGattUUID(serviceId),
732
+ BleNitroManager.normalizeGattUUID(characteristicId)
733
+ );
734
+ }
735
+
674
736
  /**
675
737
  * Check if Bluetooth is enabled
676
738
  * @returns returns Boolean according to Bluetooth state
@@ -92,6 +92,8 @@ export interface NativeBleNitro extends HybridObject<{ ios: 'swift'; android: 'k
92
92
 
93
93
  // Service discovery
94
94
  discoverServices(deviceId: string, callback: OperationCallback): void;
95
+ /** Discover services and wait for all characteristic discovery to complete before resolving. */
96
+ discoverServicesWithCharacteristics(deviceId: string, callback: OperationCallback): void;
95
97
  getServices(deviceId: string): string[];
96
98
  getCharacteristics(deviceId: string, serviceId: string): string[];
97
99
 
@@ -100,6 +102,7 @@ export interface NativeBleNitro extends HybridObject<{ ios: 'swift'; android: 'k
100
102
  writeCharacteristic(deviceId: string, serviceId: string, characteristicId: string, data: BLEValue, withResponse: boolean, callback: WriteCharacteristicCallback): void;
101
103
  subscribeToCharacteristic(deviceId: string, serviceId: string, characteristicId: string, updateCallback: CharacteristicCallback, completionCallback: OperationCallback): void;
102
104
  unsubscribeFromCharacteristic(deviceId: string, serviceId: string, characteristicId: string, callback: OperationCallback): void;
105
+ isSubscribedToCharacteristic(deviceId: string, serviceId: string, characteristicId: string): boolean;
103
106
 
104
107
  // Bluetooth state management
105
108
  requestBluetoothEnable(callback: OperationCallback): void;