unified-ble-manager 4.0.13 → 4.0.15

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 (78) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/MIGRATION_4.0.md +1 -1
  3. package/README.md +1 -1
  4. package/RELEASE.md +21 -8
  5. package/SBOM.cdx.json +5 -5
  6. package/THIRD_PARTY_LICENSES.json +2 -2
  7. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt +40 -2
  8. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +36 -0
  9. package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +139 -1
  10. package/docs/ELECTRON.md +1 -1
  11. package/docs/EXPO_PLUGIN.md +1 -1
  12. package/docs/GAPS.4.0.md +1 -1
  13. package/docs/GETTING_STARTED.md +3 -3
  14. package/docs/NODE.md +1 -1
  15. package/docs/WEB.md +1 -1
  16. package/docs/generated/PLATFORM_SUPPORT.md +1 -1
  17. package/lib/commonjs/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js +3 -0
  18. package/lib/commonjs/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js.map +1 -1
  19. package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js +27 -2
  20. package/lib/commonjs/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
  21. package/lib/commonjs/backends/winrt/winrt-backend.js +52 -10
  22. package/lib/commonjs/backends/winrt/winrt-backend.js.map +1 -1
  23. package/lib/commonjs/backends/winrt/winrt-handles.js +1 -1
  24. package/lib/commonjs/backends/winrt/winrt-handles.js.map +1 -1
  25. package/lib/commonjs/implementation-version.js +1 -1
  26. package/lib/commonjs/native-protocol/rn-android-boundary.js +44 -0
  27. package/lib/commonjs/native-protocol/rn-android-boundary.js.map +1 -1
  28. package/lib/module/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js +3 -0
  29. package/lib/module/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.js.map +1 -1
  30. package/lib/module/backends/corebluetooth/corebluetooth-backend.js +27 -2
  31. package/lib/module/backends/corebluetooth/corebluetooth-backend.js.map +1 -1
  32. package/lib/module/backends/winrt/winrt-backend.js +52 -10
  33. package/lib/module/backends/winrt/winrt-backend.js.map +1 -1
  34. package/lib/module/backends/winrt/winrt-handles.js +1 -1
  35. package/lib/module/backends/winrt/winrt-handles.js.map +1 -1
  36. package/lib/module/implementation-version.js +1 -1
  37. package/lib/module/native-protocol/rn-android-boundary.js +44 -0
  38. package/lib/module/native-protocol/rn-android-boundary.js.map +1 -1
  39. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
  40. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts +1 -0
  41. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts +2 -0
  43. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  45. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
  46. package/lib/typescript/commonjs/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  47. package/lib/typescript/commonjs/src/backends/winrt/winrt-backend.d.ts +3 -0
  48. package/lib/typescript/commonjs/src/backends/winrt/winrt-backend.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/src/backends/winrt/winrt-provider.d.ts +1 -1
  50. package/lib/typescript/commonjs/src/implementation-version.d.ts +1 -1
  51. package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts +8 -0
  52. package/lib/typescript/commonjs/src/native-protocol/rn-android-boundary.d.ts.map +1 -1
  53. package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
  54. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts +1 -0
  55. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.d.ts.map +1 -1
  56. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts +2 -0
  57. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-backend.d.ts.map +1 -1
  58. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  59. package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
  60. package/lib/typescript/module/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  61. package/lib/typescript/module/src/backends/winrt/winrt-backend.d.ts +3 -0
  62. package/lib/typescript/module/src/backends/winrt/winrt-backend.d.ts.map +1 -1
  63. package/lib/typescript/module/src/backends/winrt/winrt-provider.d.ts +1 -1
  64. package/lib/typescript/module/src/implementation-version.d.ts +1 -1
  65. package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts +8 -0
  66. package/lib/typescript/module/src/native-protocol/rn-android-boundary.d.ts.map +1 -1
  67. package/native/PREBUILDS.json +5 -5
  68. package/native/electron/corebluetooth/prebuilds/darwin-arm64/unified_ble_corebluetooth.node +0 -0
  69. package/native/electron/corebluetooth/prebuilds/darwin-x64/unified_ble_corebluetooth.node +0 -0
  70. package/native/electron/winrt/prebuilds/win32-arm64/unified_ble_winrt.node +0 -0
  71. package/native/electron/winrt/prebuilds/win32-x64/unified_ble_winrt.node +0 -0
  72. package/package.json +1 -1
  73. package/src/backends/corebluetooth/corebluetooth-adapter-loss-cleanup.ts +4 -0
  74. package/src/backends/corebluetooth/corebluetooth-backend.ts +27 -2
  75. package/src/backends/winrt/winrt-backend.ts +57 -10
  76. package/src/backends/winrt/winrt-handles.ts +1 -1
  77. package/src/implementation-version.ts +1 -1
  78. package/src/native-protocol/rn-android-boundary.ts +60 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,35 @@ All notable changes to `unified-ble-manager` are documented here.
6
6
 
7
7
  No changes yet.
8
8
 
9
+ ## [4.0.15] - 2026-09-02
10
+
11
+ ### Fixes
12
+
13
+ - Silence exactly one expected late React Native Android terminal when a peer
14
+ is lost while an operation such as GATT discovery is still settling. The
15
+ boundary records only operations it explicitly rejected for that peer,
16
+ bounds and lifecycle-scopes those records, and continues to report unknown
17
+ or duplicate terminals as protocol diagnostics.
18
+
19
+ ## [4.0.14] - 2026-09-02
20
+
21
+ ### Fixes
22
+
23
+ - Release Android GATT ownership immediately when Bluetooth powers off or
24
+ resets instead of waiting for a `STATE_DISCONNECTED` callback that Android
25
+ may never deliver. Failed native closes remain retained and retry when the
26
+ adapter returns, protocol routes are invalidated before adapter-state
27
+ publication, and an ordinary disconnect timeout now also publishes the
28
+ missing terminal state before cleanup. A cleanup unsubscribe after adapter
29
+ loss is idempotent when native ownership is already gone.
30
+ - Terminalize adapter-loss connections exactly once with the typed `adapter`
31
+ reason when a native CoreBluetooth-style or WinRT disconnect completes
32
+ without a platform loss callback. Delayed native callbacks are ignored, and
33
+ WinRT connection paths retain the attachment that created them.
34
+ - Treat an operation that is still settling after adapter loss as expected
35
+ quarantine rather than a cleanup error. Cleanup still waits and retries, but
36
+ only an actual native release failure produces an error diagnostic.
37
+
9
38
  ## [4.0.13] - 2026-09-01
10
39
 
11
40
  ### Fixes
package/MIGRATION_4.0.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Migrating from react-native-ble-plx
4
4
 
5
- This source targets `4.0.13`. The `4.0.0` release is a new package and a new contract. It is **not a source-compatible rename**. There is no `new BleManager()` facade, no Base64 characteristic values, and no public transaction IDs.
5
+ This source targets `4.0.15`. The `4.0.0` release is a new package and a new contract. It is **not a source-compatible rename**. There is no `new BleManager()` facade, no Base64 characteristic values, and no public transaction IDs.
6
6
 
7
7
  This page is for a React Native app that already uses `react-native-ble-plx`. Web, Electron, Node, and Tauri are new hosts — use those pages after you understand the RN rewrite.
8
8
 
package/README.md CHANGED
@@ -18,7 +18,7 @@ root import does not pick a radio. Package SemVer and backend support labels are
18
18
  independent: each radio backend keeps its evidence-derived label. See
19
19
  [`docs/PLATFORMS.md`](docs/PLATFORMS.md).
20
20
 
21
- This source tree is versioned `4.0.13`. Install the exact version shown in the npm
21
+ This source tree is versioned `4.0.15`. Install the exact version shown in the npm
22
22
  registry. During release preparation, the version in `package.json` can be ahead
23
23
  of npm until the matching tag-driven workflow publishes it; the registry and
24
24
  GitHub release remain authoritative.
package/RELEASE.md CHANGED
@@ -81,7 +81,7 @@ Active `4.0.0-rc.*` release-train candidates publish to npm `latest` so a bare `
81
81
 
82
82
  On release day, set `release_candidate` to the exact candidate required by the
83
83
  release plan. RC2, RC3, RC4, `4.0.0-rc.4.1`, and RC5 are already immutable
84
- once tagged. Stable `4.0.0` through `4.0.12` are immutable. `4.0.13` is the current train head.
84
+ once tagged. Stable `4.0.0` through `4.0.14` are immutable. `4.0.15` is the current train head.
85
85
 
86
86
  ```sh
87
87
  release_candidate=4.0.0-rc.N
@@ -133,9 +133,9 @@ The `v4.0.3` tag is immutable published history. Do not recreate or move it.
133
133
  git tag -a v4.0.3 -m "v4.0.3"
134
134
  ```
135
135
 
136
- ## Releasing 4.0.13
136
+ ## Releasing 4.0.15
137
137
 
138
- The `v4.0.13` tag must identify the exact current `main` commit after canonical
138
+ The `v4.0.15` tag must identify the exact current `main` commit after canonical
139
139
  CI passes. Do not tag this feature branch directly.
140
140
 
141
141
  ```sh
@@ -144,15 +144,28 @@ git checkout main
144
144
  git pull --ff-only origin main
145
145
 
146
146
  test "$(git branch --show-current)" = "main"
147
- test "$(node -p "require('./package.json').version")" = "4.0.13"
147
+ test "$(node -p "require('./package.json').version")" = "4.0.15"
148
148
  git diff --exit-code
149
149
  git diff --cached --exit-code
150
150
 
151
- git tag -a v4.0.13 -m "v4.0.13"
152
- git push origin v4.0.13
151
+ git tag -a v4.0.15 -m "v4.0.15"
152
+ git push origin v4.0.15
153
153
  ```
154
154
 
155
- Before tagging, confirm release-note extraction finds `## [4.0.13]`.
155
+ Before tagging, confirm release-note extraction finds `## [4.0.15]`.
156
+
157
+ ## Releasing 4.0.14
158
+
159
+ The `v4.0.14` tag is immutable history, but its publish workflow was cancelled
160
+ before the npm version check and publish steps. Do not recreate or move it.
161
+
162
+ ## Releasing 4.0.13
163
+
164
+ The `v4.0.13` tag is immutable published history. Do not recreate or move it.
165
+
166
+ ```sh
167
+ git tag -a v4.0.13 -m "v4.0.13"
168
+ ```
156
169
 
157
170
  ## Releasing 4.0.12
158
171
 
@@ -327,7 +340,7 @@ a green publish job and a package a consumer can actually install are not the
327
340
  same claim.
328
341
 
329
342
  ```sh
330
- version=4.0.12
343
+ version=4.0.15
331
344
 
332
345
  npm view "unified-ble-manager@$version" version
333
346
  npm view unified-ble-manager dist-tags --json
package/SBOM.cdx.json CHANGED
@@ -6,20 +6,20 @@
6
6
  "metadata": {
7
7
  "component": {
8
8
  "type": "library",
9
- "bom-ref": "pkg:npm/unified-ble-manager@4.0.13",
9
+ "bom-ref": "pkg:npm/unified-ble-manager@4.0.15",
10
10
  "name": "unified-ble-manager",
11
- "version": "4.0.13",
11
+ "version": "4.0.15",
12
12
  "licenses": [
13
13
  {
14
14
  "expression": "Apache-2.0"
15
15
  }
16
16
  ],
17
- "purl": "pkg:npm/unified-ble-manager@4.0.13"
17
+ "purl": "pkg:npm/unified-ble-manager@4.0.15"
18
18
  },
19
19
  "properties": [
20
20
  {
21
21
  "name": "unified-ble-manager:pnpm-lock-sha256",
22
- "value": "58c2b09c18f10de1e250a357e5ce9445fe6bcace614e3778bc60166b9db31fa1"
22
+ "value": "fea8b9fea3dca31d966c33e5942c5fb0f179c492a8462f90482d0c5dc1bd86d2"
23
23
  },
24
24
  {
25
25
  "name": "unified-ble-manager:dependency-scope",
@@ -537,7 +537,7 @@
537
537
  "dependsOn": []
538
538
  },
539
539
  {
540
- "ref": "pkg:npm/unified-ble-manager@4.0.13",
540
+ "ref": "pkg:npm/unified-ble-manager@4.0.15",
541
541
  "dependsOn": [
542
542
  "pkg:npm/%40babel/runtime@7.29.7",
543
543
  "pkg:npm/node-addon-api@8.9.0",
@@ -4,12 +4,12 @@
4
4
  "schemaVersion": "1.0.0",
5
5
  "package": {
6
6
  "name": "unified-ble-manager",
7
- "version": "4.0.13"
7
+ "version": "4.0.15"
8
8
  },
9
9
  "source": {
10
10
  "method": "pnpm-lock production graph with installed-manifest license audit",
11
11
  "lockfile": "pnpm-lock.yaml",
12
- "lockfileSha256": "58c2b09c18f10de1e250a357e5ce9445fe6bcace614e3778bc60166b9db31fa1"
12
+ "lockfileSha256": "fea8b9fea3dca31d966c33e5942c5fb0f179c492a8462f90482d0c5dc1bd86d2"
13
13
  },
14
14
  "reviewedOverrides": [
15
15
  {
@@ -38,7 +38,8 @@ class UnifiedBleProtocolAndroidDispatcher(
38
38
  private var attachmentRecord: ProtocolWireRecord? = null
39
39
 
40
40
  init {
41
- radio.onAdapterState = {
41
+ radio.onAdapterState = { adapterState ->
42
+ clearGattProtocolOwnershipForAdapterLoss(adapterState)
42
43
  emitCurrentAdapterState()
43
44
  }
44
45
  radio.onCleanupFailure = { failure ->
@@ -140,6 +141,33 @@ class UnifiedBleProtocolAndroidDispatcher(
140
141
  }
141
142
  }
142
143
 
144
+ private fun clearGattProtocolOwnershipForAdapterLoss(adapterState: String) {
145
+ val state = radio.currentProtocolAdapterState()
146
+ if (state.availability == "available" && state.authorization == "granted" && state.power == "on") return
147
+ val failure = when (adapterState) {
148
+ "PoweredOff" -> AdapterLossConnectFailure(
149
+ "adapterPoweredOff",
150
+ "Android Bluetooth was powered off while the connection was pending"
151
+ )
152
+ "Resetting" -> AdapterLossConnectFailure(
153
+ "adapterResetting",
154
+ "Android Bluetooth reset while the connection was pending"
155
+ )
156
+ else -> AdapterLossConnectFailure(
157
+ "adapterUnavailable",
158
+ "Android Bluetooth became unavailable while the connection was pending"
159
+ )
160
+ }
161
+ pendingConnects.entries.toList().forEach { entry ->
162
+ if (pendingConnects.remove(entry.key, entry.value)) {
163
+ emitFailure(entry.value, failure.code, failure.message)
164
+ }
165
+ }
166
+ establishedConnections.clear()
167
+ activeDatabases.clear()
168
+ activeSubscriptions.clear()
169
+ }
170
+
143
171
  fun emitCurrentAdapterState() {
144
172
  val state = radio.currentProtocolAdapterState()
145
173
  val fields = mutableMapOf<Int, ProtocolWireValue>(
@@ -564,6 +592,11 @@ class UnifiedBleProtocolAndroidDispatcher(
564
592
  }
565
593
 
566
594
  private fun subscribe(command: ProtocolWireRecord, enable: Boolean) {
595
+ val subscriptionId = command.requiredString(7)
596
+ if (!enable && !activeSubscriptions.containsKey(subscriptionId)) {
597
+ emitSuccess(command, "unsubscribed")
598
+ return
599
+ }
567
600
  val endpoint = characteristicEndpoint(command.requiredRecord(4))
568
601
  val radioOperationId = radio.setNotifyExact(
569
602
  endpoint.deviceId,
@@ -597,7 +630,6 @@ class UnifiedBleProtocolAndroidDispatcher(
597
630
  }
598
631
  return@fold
599
632
  }
600
- val subscriptionId = command.requiredString(7)
601
633
  if (enable) {
602
634
  activeSubscriptions[subscriptionId] = SubscriptionRoute(
603
635
  subscriptionId,
@@ -1132,6 +1164,11 @@ class UnifiedBleProtocolAndroidDispatcher(
1132
1164
  }
1133
1165
  }
1134
1166
 
1167
+ private data class AdapterLossConnectFailure(
1168
+ val code: String,
1169
+ val message: String
1170
+ )
1171
+
1135
1172
  private fun requirePhyAvailable() {
1136
1173
  check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1137
1174
  "Android PHY requires API 26"
@@ -1158,6 +1195,7 @@ internal fun connectionLostEvent(
1158
1195
  8 to ProtocolWireValue.SignedIntegerValue(status.toLong())
1159
1196
  )
1160
1197
  )
1198
+
1161
1199
  return ProtocolWireRecord(
1162
1200
  RecordKind.EVENT,
1163
1201
  mapOf(
@@ -150,6 +150,11 @@ internal fun bondedPeerAdapterReadiness(
150
150
  }
151
151
  }
152
152
 
153
+ internal fun requiresImmediateGattTeardownOnAdapterState(state: Int?): Boolean =
154
+ state == BluetoothAdapter.STATE_OFF ||
155
+ state == BluetoothAdapter.STATE_TURNING_OFF ||
156
+ state == BluetoothAdapter.STATE_TURNING_ON
157
+
153
158
  /** Normalize the platform snapshot before it crosses the Android protocol boundary. */
154
159
  internal fun normalizeBondedPeerSnapshots(
155
160
  peers: Iterable<BondedPeerSnapshot>
@@ -416,6 +421,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
416
421
  override fun onReceive(ctx: Context?, intent: Intent?) {
417
422
  if (intent?.action != BluetoothAdapter.ACTION_STATE_CHANGED) return
418
423
  val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
424
+ handleAdapterStateTransition(state)
419
425
  onAdapterState?.invoke(mapAdapterState(state))
420
426
  }
421
427
  }
@@ -430,6 +436,23 @@ class OwnedAndroidGattRadio(private val context: Context) {
430
436
  adapterStateReceiver = receiver
431
437
  }
432
438
 
439
+ private fun handleAdapterStateTransition(state: Int) {
440
+ if (state == BluetoothAdapter.STATE_ON) {
441
+ pendingGattTeardowns.keys.toList().forEach { key -> retryGattTeardown(key) }
442
+ return
443
+ }
444
+ if (!requiresImmediateGattTeardownOnAdapterState(state)) return
445
+
446
+ pendingReconnect.clear()
447
+ (gatts.keys + pendingGattTeardowns.keys).toSet().forEach { key ->
448
+ failPendingForDevice(key, "adapter unavailable")
449
+ val owner = pendingGattTeardowns[key]
450
+ val gatt = gatts[key] ?: owner?.gatt ?: return@forEach
451
+ val teardownFailure = completeGattTeardown(key, gatt)
452
+ pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)
453
+ }
454
+ }
455
+
433
456
  internal fun unregisterAdapterStateReceiver(): OwnedRadioTeardownFailure? {
434
457
  val receiver = adapterStateReceiver ?: return null
435
458
  try {
@@ -838,6 +861,18 @@ class OwnedAndroidGattRadio(private val context: Context) {
838
861
  onComplete(failure)
839
862
  return failure
840
863
  }
864
+ if (pendingGattTeardowns[key] != null) {
865
+ val teardownFailure = retryGattTeardown(key)
866
+ val failure = cleanupFailure ?: teardownFailure
867
+ onComplete(failure)
868
+ return failure
869
+ }
870
+ if (requiresImmediateGattTeardownOnAdapterState(adapter?.state)) {
871
+ val teardownFailure = completeGattTeardown(key, g)
872
+ val failure = cleanupFailure ?: teardownFailure
873
+ onComplete(failure)
874
+ return failure
875
+ }
841
876
  // R3-F003: only disconnect(); close() waits for STATE_DISCONNECTED (or safety timeout).
842
877
  pendingDisconnectCallbacks[key] = onComplete
843
878
  scheduleSafeClose(key, g)
@@ -923,6 +958,7 @@ class OwnedAndroidGattRadio(private val context: Context) {
923
958
  Runnable {
924
959
  if (gatts[key] === gatt) {
925
960
  OwnedAndroidLog.e("GATT close safety timeout for $key (DISCONNECTED never arrived)")
961
+ dispatchConnectionState(key, false, BluetoothGatt.GATT_FAILURE)
926
962
  failPendingForDevice(key, "disconnected timeout")
927
963
  val teardownFailure = completeGattTeardown(key, gatt)
928
964
  pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)
@@ -11,6 +11,7 @@ import com.sfourdrinier.unifiedblemanager.radio.OwnedAndroidSubscriptionOwnershi
11
11
  import com.sfourdrinier.unifiedblemanager.radio.BondedPeerSnapshot
12
12
  import com.sfourdrinier.unifiedblemanager.radio.normalizeBondedPeerSnapshots
13
13
  import com.sfourdrinier.unifiedblemanager.radio.bondedPeerAdapterReadiness
14
+ import com.sfourdrinier.unifiedblemanager.radio.requiresImmediateGattTeardownOnAdapterState
14
15
  import org.junit.Assert.assertEquals
15
16
  import org.junit.Assert.assertArrayEquals
16
17
  import org.junit.Assert.assertFalse
@@ -26,6 +27,7 @@ import java.util.concurrent.CountDownLatch
26
27
  import java.util.concurrent.atomic.AtomicInteger
27
28
  import kotlin.concurrent.thread
28
29
  import android.bluetooth.BluetoothDevice
30
+ import android.bluetooth.BluetoothAdapter
29
31
  import android.bluetooth.le.ScanSettings
30
32
 
31
33
  class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
@@ -477,7 +479,7 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
477
479
  val consumerAndroidDirectory = File(consumerRoot, "android")
478
480
  val packagedSource = File(consumerRoot, "node_modules/unified-ble-manager/$relativePath")
479
481
  consumerAndroidDirectory.mkdirs()
480
- packagedSource.parentFile.mkdirs()
482
+ requireNotNull(packagedSource.parentFile).mkdirs()
481
483
  packagedSource.writeText("installed-package-source")
482
484
 
483
485
  try {
@@ -788,6 +790,142 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
788
790
  assertEquals(1, secondStarted)
789
791
  }
790
792
 
793
+ @Test
794
+ fun gattSafetyClosePublishesConnectionLossBeforeDroppingNativeOwnership() {
795
+ val radio = readAndroidSource(
796
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
797
+ )
798
+ val timeout = radio.substring(
799
+ radio.indexOf("private fun scheduleSafeClose"),
800
+ radio.indexOf("private fun cancelSafeClose")
801
+ )
802
+
803
+ val connectionLossIndex = timeout.indexOf("dispatchConnectionState(key, false, BluetoothGatt.GATT_FAILURE)")
804
+ val failPendingIndex = timeout.indexOf("failPendingForDevice(key, \"disconnected timeout\")")
805
+ val teardownIndex = timeout.indexOf("val teardownFailure = completeGattTeardown(key, gatt)")
806
+ assertTrue(connectionLossIndex >= 0)
807
+ assertTrue(connectionLossIndex < failPendingIndex)
808
+ assertTrue(connectionLossIndex < teardownIndex)
809
+ }
810
+
811
+ @Test
812
+ fun adapterLossClosesGattOwnersBeforeForwardingTheState() {
813
+ val radio = readAndroidSource(
814
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
815
+ )
816
+ val receiver = radio.substring(
817
+ radio.indexOf("fun registerAdapterStateReceiver()"),
818
+ radio.indexOf("internal fun unregisterAdapterStateReceiver")
819
+ )
820
+
821
+ val teardownIndex = receiver.indexOf("handleAdapterStateTransition(state)")
822
+ val forwardIndex = receiver.indexOf("onAdapterState?.invoke(mapAdapterState(state))")
823
+ assertTrue(teardownIndex >= 0)
824
+ assertTrue(teardownIndex < forwardIndex)
825
+ }
826
+
827
+ @Test
828
+ fun onlyUnavailableAdapterTransitionsRequireImmediateGattTeardown() {
829
+ assertFalse(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_ON))
830
+ assertTrue(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_OFF))
831
+ assertTrue(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_TURNING_OFF))
832
+ assertTrue(requiresImmediateGattTeardownOnAdapterState(BluetoothAdapter.STATE_TURNING_ON))
833
+ }
834
+
835
+ @Test
836
+ fun adapterLossTeardownDoesNotWaitForGattDisconnectedCallback() {
837
+ val radio = readAndroidSource(
838
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
839
+ )
840
+ val transition = radio.substring(
841
+ radio.indexOf("private fun handleAdapterStateTransition("),
842
+ radio.indexOf("internal fun unregisterAdapterStateReceiver")
843
+ )
844
+
845
+ assertTrue(transition.contains("pendingReconnect.clear()"))
846
+ assertTrue(transition.contains("failPendingForDevice(key, \"adapter unavailable\")"))
847
+ assertTrue(transition.contains("completeGattTeardown(key, gatt)"))
848
+ assertTrue(transition.contains("pendingDisconnectCallbacks.remove(key)?.invoke(teardownFailure)"))
849
+ assertFalse(transition.contains("scheduleSafeClose"))
850
+ assertFalse(transition.contains("dispatchConnectionState"))
851
+ }
852
+
853
+ @Test
854
+ fun adapterRecoveryRetriesRetainedGattTeardownOwnership() {
855
+ val radio = readAndroidSource(
856
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
857
+ )
858
+ val transition = radio.substring(
859
+ radio.indexOf("private fun handleAdapterStateTransition("),
860
+ radio.indexOf("internal fun unregisterAdapterStateReceiver")
861
+ )
862
+
863
+ assertTrue(transition.contains("BluetoothAdapter.STATE_ON"))
864
+ assertTrue(transition.contains("pendingGattTeardowns.keys.toList()"))
865
+ assertTrue(transition.contains("retryGattTeardown(key)"))
866
+ }
867
+
868
+ @Test
869
+ fun disconnectRetriesRetainedOrUnavailableGattWithoutSchedulingSafetyClose() {
870
+ val radio = readAndroidSource(
871
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt"
872
+ )
873
+ val disconnect = radio.substring(
874
+ radio.indexOf("internal fun disconnect("),
875
+ radio.indexOf("private fun openGatt")
876
+ )
877
+
878
+ val retainedIndex = disconnect.indexOf("pendingGattTeardowns[key]")
879
+ val unavailableIndex = disconnect.indexOf("requiresImmediateGattTeardownOnAdapterState(adapter?.state)")
880
+ val safetyCloseIndex = disconnect.indexOf("scheduleSafeClose(key, g)")
881
+ assertTrue(retainedIndex >= 0)
882
+ assertTrue(unavailableIndex > retainedIndex)
883
+ assertTrue(safetyCloseIndex > unavailableIndex)
884
+ }
885
+
886
+ @Test
887
+ fun adapterLossClearsProtocolGattRoutesBeforePublishingAdapterState() {
888
+ val dispatcher = readAndroidSource(
889
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt"
890
+ )
891
+ val callback = dispatcher.substring(
892
+ dispatcher.indexOf("radio.onAdapterState = {"),
893
+ dispatcher.indexOf("radio.onCleanupFailure")
894
+ )
895
+ val clearIndex = callback.indexOf("clearGattProtocolOwnershipForAdapterLoss(adapterState)")
896
+ val emitIndex = callback.indexOf("emitCurrentAdapterState()")
897
+ assertTrue(clearIndex >= 0)
898
+ assertTrue(clearIndex < emitIndex)
899
+
900
+ val cleanup = dispatcher.substring(
901
+ dispatcher.indexOf("private fun clearGattProtocolOwnershipForAdapterLoss(adapterState: String)"),
902
+ dispatcher.indexOf("fun emitCurrentAdapterState()")
903
+ )
904
+ assertTrue(cleanup.contains("establishedConnections.clear()"))
905
+ assertTrue(cleanup.contains("activeDatabases.clear()"))
906
+ assertTrue(cleanup.contains("activeSubscriptions.clear()"))
907
+ assertTrue(cleanup.contains("pendingConnects.entries.toList()"))
908
+ assertTrue(cleanup.contains("pendingConnects.remove(entry.key, entry.value)"))
909
+ assertTrue(cleanup.contains("emitFailure(entry.value, failure.code, failure.message)"))
910
+ }
911
+
912
+ @Test
913
+ fun unsubscribeIsIdempotentAfterAdapterLossClearsNativeOwnership() {
914
+ val dispatcher = readAndroidSource(
915
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt"
916
+ )
917
+ val subscribe = dispatcher.substring(
918
+ dispatcher.indexOf("private fun subscribe(command: ProtocolWireRecord, enable: Boolean)"),
919
+ dispatcher.indexOf("private fun destroy(command: ProtocolWireRecord)")
920
+ )
921
+ val absentRouteIndex = subscribe.indexOf("!enable && !activeSubscriptions.containsKey(subscriptionId)")
922
+ val successIndex = subscribe.indexOf("emitSuccess(command, \"unsubscribed\")")
923
+ val nativeIndex = subscribe.indexOf("radio.setNotifyExact(")
924
+ assertTrue(absentRouteIndex >= 0)
925
+ assertTrue(successIndex > absentRouteIndex)
926
+ assertTrue(nativeIndex > successIndex)
927
+ }
928
+
791
929
  @Test
792
930
  fun destroyBeforePumpCallsEveryQueuedCancellationAndNeverStartsThem() {
793
931
  val scheduled = ArrayDeque<() -> Unit>()
package/docs/ELECTRON.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Main owns the radio. The renderer uses a versioned IPC client and never loads a native addon.
6
6
 
7
- This source targets `4.0.13`. Tagged releases ship Node-API v8 prebuilds covering macOS and Windows `arm64`/`x64` for both Node and modern Electron.
7
+ This source targets `4.0.15`. Tagged releases ship Node-API v8 prebuilds covering macOS and Windows `arm64`/`x64` for both Node and modern Electron.
8
8
 
9
9
  `unified-ble-manager/electron/main` and
10
10
  `unified-ble-manager/electron/renderer` are the only Electron entrypoints.
@@ -7,7 +7,7 @@ start a radio, request runtime permissions during prebuild, or prove physical
7
7
  radio/restoration reliability. Expo Go is not a supported BLE execution
8
8
  environment because it cannot contain this native module.
9
9
 
10
- Use the v2 plugin options in this `4.0.13` source. Those options match
10
+ Use the v2 plugin options in this `4.0.15` source. Those options match
11
11
  the schema introduced at `4.0.0-rc.4`. Expo Go cannot load this native module.
12
12
 
13
13
  ## Installation and development build
package/docs/GAPS.4.0.md CHANGED
@@ -14,7 +14,7 @@ This file tracks platform code, CI, package, lab, and live-radio evidence. It do
14
14
 
15
15
  The clean-baseline contract, unified core, public manager, deterministic backend, TCK, native protocol, first-party backend implementations, host-isolated package exports, SDK/CLI, and legacy-absence gates exist in the 4.0 source. Passing deterministic, compile, ABI, or package tests are implementation proof; they do not become physical-radio support evidence unless a retained record proves the corresponding live scenario.
16
16
 
17
- This source targets `unified-ble-manager@4.0.13`; the npm registry and release
17
+ This source targets `unified-ble-manager@4.0.15`; the npm registry and release
18
18
  provenance, not a source version string, determine whether it is published.
19
19
  Earlier stable releases remain immutable published history. Backend support labels remain
20
20
  evidence-derived. This package does not rewrite the evidence inventory: a backend remains at the support level
@@ -4,7 +4,7 @@
4
4
 
5
5
  This page gets you to a first scan, connect, read, notify, and teardown on React Native. Other hosts are linked at the bottom. The root import does not turn Bluetooth on.
6
6
 
7
- This source targets `4.0.13`; verify the published version in the npm registry.
7
+ This source targets `4.0.15`; verify the published version in the npm registry.
8
8
 
9
9
  ## Pick a host
10
10
 
@@ -38,11 +38,11 @@ request runtime permissions on Android 12+, add
38
38
  #### Expo / CNG v2
39
39
 
40
40
  The Expo v2 schema and `unified-ble-manager/expo` factory are in this source.
41
- After the npm registry lists `4.0.13`, install that exact version and keep it in
41
+ After the npm registry lists `4.0.15`, install that exact version and keep it in
42
42
  your lockfile while validating the native build:
43
43
 
44
44
  ```sh
45
- pnpm add unified-ble-manager@4.0.13
45
+ pnpm add unified-ble-manager@4.0.15
46
46
  ```
47
47
 
48
48
  The package does not run in Expo Go.
package/docs/NODE.md CHANGED
@@ -10,7 +10,7 @@ The root import does not open an adapter. Pick one backend:
10
10
  | `unified-ble-manager/node/winrt` | Windows |
11
11
  | `unified-ble-manager/node/bluez` | Linux (needs `dbus-next@^0.10.2` in the app) |
12
12
 
13
- This source targets `4.0.13`. Tagged releases ship Node-API v8 prebuilds for macOS and Windows on `arm64` and `x64`. A normal install should not compile native code. BlueZ talks D-Bus and has no addon.
13
+ This source targets `4.0.15`. Tagged releases ship Node-API v8 prebuilds for macOS and Windows on `arm64` and `x64`. A normal install should not compile native code. BlueZ talks D-Bus and has no addon.
14
14
 
15
15
  ## One-call factories
16
16
 
package/docs/WEB.md CHANGED
@@ -6,7 +6,7 @@ Use `unified-ble-manager/web` to run the public UBM manager directly in a browse
6
6
 
7
7
  The complete runnable TypeScript/Vite application is in [`example-web/`](../example-web/).
8
8
 
9
- This guide targets `4.0.13`. Web Bluetooth support still depends on the browser, operating system, adapter, and peripheral. UBM reports those runtime boundaries; it does not fabricate a fallback backend.
9
+ This guide targets `4.0.15`. Web Bluetooth support still depends on the browser, operating system, adapter, and peripheral. UBM reports those runtime boundaries; it does not fabricate a fallback backend.
10
10
 
11
11
  ## Requirements
12
12
 
@@ -6,7 +6,7 @@ This page is generated by `scripts/docs/generate-platform-support.js` from valid
6
6
 
7
7
  ## Current package
8
8
 
9
- `unified-ble-manager@4.0.13`
9
+ `unified-ble-manager@4.0.15`
10
10
 
11
11
  No evidence record is bound to this exact package version and artifact. Consequently, this generated page makes no Preview, Live Preview, Supported, or Reliability-qualified platform claim for the current package.
12
12
 
@@ -39,12 +39,15 @@ async function releaseCoreBluetoothAdapterLossResources(state) {
39
39
  failures.push(...cleanup.failures);
40
40
  }
41
41
  for (const record of [...state.connections.values()]) {
42
+ const previous = record.state === 'disconnecting' ? 'disconnecting' : 'connected';
42
43
  record.state = 'disconnecting';
43
44
  try {
44
45
  const cleanup = await state.disconnectNative(record, 'direct-gatt.adapter-loss.disconnect', true);
45
46
  failures.push(...cleanup.failures);
46
47
  if (cleanup.state === 'release-failed') {
47
48
  record.state = 'connected';
49
+ } else {
50
+ state.terminalizeAdapterLossConnection(record, previous);
48
51
  }
49
52
  } catch (error) {
50
53
  record.state = 'connected';
@@ -1 +1 @@
1
- {"version":3,"names":["_corebluetoothHandles","require","releaseCoreBluetoothAdapterLossResources","state","failures","group","scanGroup","consumer","consumers","values","releaseScanConsumerAdmission","stream","closeWithReason","cleanup","stopNativeScan","push","clear","clearScanGroup","error","cleanupFailureDetail","physical","subscriptions","subscription","removed","gattOperations","stopPhysicalSubscription","record","connections","disconnectNative","length","releasedCleanup","Object","freeze"],"sourceRoot":"../../../../src","sources":["backends/corebluetooth/corebluetooth-adapter-loss-cleanup.ts"],"mappings":";;;;;;AAGA,IAAAA,qBAAA,GAAAC,OAAA;AAHA;;AAsBA;AACO,eAAeC,wCAAwCA,CAC5DC,KAA2C,EACnB;EACxB,MAAMC,QAA0B,GAAG,EAAE;EACrC,MAAMC,KAAK,GAAGF,KAAK,CAACG,SAAS;EAC7B,IAAID,KAAK,KAAK,IAAI,EAAE;IAClBA,KAAK,CAACF,KAAK,GAAG,UAAU;IACxB,KAAK,MAAMI,QAAQ,IAAIF,KAAK,CAACG,SAAS,CAACC,MAAM,CAAC,CAAC,EAAE;MAC/CN,KAAK,CAACO,4BAA4B,CAACH,QAAQ,CAAC;MAC5CA,QAAQ,CAACI,MAAM,CAACC,eAAe,CAAC,eAAe,CAAC;IAClD;IACA,IAAI;MACF,MAAMC,OAAO,GAAG,MAAMV,KAAK,CAACW,cAAc,CAACT,KAAK,EAAE,oCAAoC,CAAC;MACvF,IAAIQ,OAAO,CAACV,KAAK,KAAK,gBAAgB,EAAE;QACtCC,QAAQ,CAACW,IAAI,CAAC,GAAGF,OAAO,CAACT,QAAQ,CAAC;MACpC,CAAC,MAAM;QACLC,KAAK,CAACG,SAAS,CAACQ,KAAK,CAAC,CAAC;QACvBb,KAAK,CAACc,cAAc,CAACZ,KAAK,CAAC;MAC7B;IACF,CAAC,CAAC,OAAOa,KAAK,EAAE;MACdd,QAAQ,CAACW,IAAI,CAAC,IAAAI,0CAAoB,EAAC,MAAM,EAAE,oCAAoC,EAAED,KAAK,CAAC,CAAC;IAC1F;EACF;EACA,KAAK,MAAME,QAAQ,IAAI,CAAC,GAAGjB,KAAK,CAACkB,aAAa,CAACZ,MAAM,CAAC,CAAC,CAAC,EAAE;IACxD,KAAK,MAAMa,YAAY,IAAIF,QAAQ,CAACZ,SAAS,EAAE;MAC7Cc,YAAY,CAACX,MAAM,CAACC,eAAe,CAAC,eAAe,CAAC;MACpDU,YAAY,CAACC,OAAO,GAAG,IAAI;IAC7B;IACAH,QAAQ,CAACZ,SAAS,CAACQ,KAAK,CAAC,CAAC;IAC1B,MAAMH,OAAO,GAAG,MAAMV,KAAK,CAACqB,cAAc,CAACC,wBAAwB,CAACL,QAAQ,CAAC;IAC7EhB,QAAQ,CAACW,IAAI,CAAC,GAAGF,OAAO,CAACT,QAAQ,CAAC;EACpC;EACA,KAAK,MAAMsB,MAAM,IAAI,CAAC,GAAGvB,KAAK,CAACwB,WAAW,CAAClB,MAAM,CAAC,CAAC,CAAC,EAAE;IACpDiB,MAAM,CAACvB,KAAK,GAAG,eAAe;IAC9B,IAAI;MACF,MAAMU,OAAO,GAAG,MAAMV,KAAK,CAACyB,gBAAgB,CAACF,MAAM,EAAE,qCAAqC,EAAE,IAAI,CAAC;MACjGtB,QAAQ,CAACW,IAAI,CAAC,GAAGF,OAAO,CAACT,QAAQ,CAAC;MAClC,IAAIS,OAAO,CAACV,KAAK,KAAK,gBAAgB,EAAE;QACtCuB,MAAM,CAACvB,KAAK,GAAG,WAAW;MAC5B;IACF,CAAC,CAAC,OAAOe,KAAK,EAAE;MACdQ,MAAM,CAACvB,KAAK,GAAG,WAAW;MAC1BC,QAAQ,CAACW,IAAI,CAAC,IAAAI,0CAAoB,EAAC,YAAY,EAAE,qCAAqC,EAAED,KAAK,CAAC,CAAC;IACjG;EACF;EACA,OAAOd,QAAQ,CAACyB,MAAM,KAAK,CAAC,GACxBC,qCAAe,GACfC,MAAM,CAACC,MAAM,CAAC;IAAE7B,KAAK,EAAE,gBAAgB;IAAEC,QAAQ,EAAE2B,MAAM,CAACC,MAAM,CAAC5B,QAAQ;EAAE,CAAC,CAAC;AACnF","ignoreList":[]}
1
+ {"version":3,"names":["_corebluetoothHandles","require","releaseCoreBluetoothAdapterLossResources","state","failures","group","scanGroup","consumer","consumers","values","releaseScanConsumerAdmission","stream","closeWithReason","cleanup","stopNativeScan","push","clear","clearScanGroup","error","cleanupFailureDetail","physical","subscriptions","subscription","removed","gattOperations","stopPhysicalSubscription","record","connections","previous","disconnectNative","terminalizeAdapterLossConnection","length","releasedCleanup","Object","freeze"],"sourceRoot":"../../../../src","sources":["backends/corebluetooth/corebluetooth-adapter-loss-cleanup.ts"],"mappings":"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","ignoreList":[]}