unified-ble-manager 4.0.17 → 4.0.19

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 (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/MIGRATION_4.0.md +1 -1
  3. package/README.md +1 -1
  4. package/RELEASE.md +44 -2
  5. package/SBOM.cdx.json +4 -4
  6. package/THIRD_PARTY_LICENSES.json +1 -1
  7. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt +4 -6
  8. package/android/src/main/java/com/sfourdrinier/unifiedblemanager/radio/OwnedAndroidGattRadio.kt +30 -7
  9. package/android/src/test/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcherLifecycleTest.kt +40 -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/ios/NativeProtocol/UnifiedBleProtocolAppleExecution.mm +5 -0
  18. package/lib/commonjs/implementation-version.js +1 -1
  19. package/lib/module/implementation-version.js +1 -1
  20. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
  21. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  22. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
  23. package/lib/typescript/commonjs/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  24. package/lib/typescript/commonjs/src/backends/winrt/winrt-provider.d.ts +1 -1
  25. package/lib/typescript/commonjs/src/implementation-version.d.ts +1 -1
  26. package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
  27. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  28. package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
  29. package/lib/typescript/module/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  30. package/lib/typescript/module/src/backends/winrt/winrt-provider.d.ts +1 -1
  31. package/lib/typescript/module/src/implementation-version.d.ts +1 -1
  32. package/native/PREBUILDS.json +5 -5
  33. package/native/electron/corebluetooth/prebuilds/darwin-arm64/unified_ble_corebluetooth.node +0 -0
  34. package/native/electron/corebluetooth/prebuilds/darwin-x64/unified_ble_corebluetooth.node +0 -0
  35. package/native/electron/winrt/prebuilds/win32-arm64/unified_ble_winrt.node +0 -0
  36. package/native/electron/winrt/prebuilds/win32-x64/unified_ble_winrt.node +0 -0
  37. package/package.json +1 -1
  38. package/src/implementation-version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ All notable changes to `unified-ble-manager` are documented here.
6
6
 
7
7
  No changes yet.
8
8
 
9
+ ## [4.0.19] - 2026-09-03
10
+
11
+ ### Fixes
12
+
13
+ - Normalize a synchronous Android notification-registration failure as
14
+ `connection.lost` when the platform already reports the GATT peer as
15
+ disconnected. This closes the callback-ordering window where a peer could
16
+ disconnect before Android delivered `onConnectionStateChange`, while
17
+ preserving ordinary registration failures as `platform.failure`.
18
+
19
+ ## [4.0.18] - 2026-09-03
20
+
21
+ ### Fixes
22
+
23
+ - Let exactly one lifecycle path complete a React Native subscription command
24
+ when peer disconnection races a late CCCD callback. Android atomically
25
+ claims the pending command before emitting a failure, while Apple ignores
26
+ subscription completions from a disconnected connection generation.
27
+
9
28
  ## [4.0.17] - 2026-09-03
10
29
 
11
30
  ### 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.17`. 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.19`. 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.17`. Install the exact version shown in the npm
21
+ This source tree is versioned `4.0.19`. 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.16` are immutable. `4.0.17` is the current train head.
84
+ once tagged. Stable `4.0.0` through `4.0.18` are immutable. `4.0.19` is the current train head.
85
85
 
86
86
  ```sh
87
87
  release_candidate=4.0.0-rc.N
@@ -109,6 +109,27 @@ The first stable tag `v4.0.0` is immutable published history. Do not recreate or
109
109
  git tag -a v4.0.0 -m "v4.0.0"
110
110
  ```
111
111
 
112
+ ## Releasing 4.0.19
113
+
114
+ The `v4.0.19` tag must identify the exact current `main` commit after canonical
115
+ CI passes. Do not tag the release branch directly.
116
+
117
+ ```sh
118
+ git fetch origin --tags
119
+ git checkout main
120
+ git pull --ff-only origin main
121
+
122
+ test "$(git branch --show-current)" = "main"
123
+ test "$(node -p "require('./package.json').version")" = "4.0.19"
124
+ git diff --exit-code
125
+ git diff --cached --exit-code
126
+
127
+ git tag -a v4.0.19 -m "v4.0.19"
128
+ git push origin v4.0.19
129
+ ```
130
+
131
+ Before tagging, confirm release-note extraction finds `## [4.0.19]`.
132
+
112
133
  ## Releasing 4.0.1
113
134
 
114
135
  The `v4.0.1` tag is immutable published history. Do not recreate or move it.
@@ -133,6 +154,27 @@ The `v4.0.3` tag is immutable published history. Do not recreate or move it.
133
154
  git tag -a v4.0.3 -m "v4.0.3"
134
155
  ```
135
156
 
157
+ ## Releasing 4.0.18
158
+
159
+ The `v4.0.18` tag must identify the exact current `main` commit after canonical
160
+ CI passes. Do not tag the release branch directly.
161
+
162
+ ```sh
163
+ git fetch origin --tags
164
+ git checkout main
165
+ git pull --ff-only origin main
166
+
167
+ test "$(git branch --show-current)" = "main"
168
+ test "$(node -p "require('./package.json').version")" = "4.0.18"
169
+ git diff --exit-code
170
+ git diff --cached --exit-code
171
+
172
+ git tag -a v4.0.18 -m "v4.0.18"
173
+ git push origin v4.0.18
174
+ ```
175
+
176
+ Before tagging, confirm release-note extraction finds `## [4.0.18]`.
177
+
136
178
  ## Releasing 4.0.17
137
179
 
138
180
  The `v4.0.17` tag must identify the exact current `main` commit after canonical
@@ -382,7 +424,7 @@ a green publish job and a package a consumer can actually install are not the
382
424
  same claim.
383
425
 
384
426
  ```sh
385
- version=4.0.17
427
+ version=4.0.19
386
428
 
387
429
  npm view "unified-ble-manager@$version" version
388
430
  npm view unified-ble-manager dist-tags --json
package/SBOM.cdx.json CHANGED
@@ -6,15 +6,15 @@
6
6
  "metadata": {
7
7
  "component": {
8
8
  "type": "library",
9
- "bom-ref": "pkg:npm/unified-ble-manager@4.0.17",
9
+ "bom-ref": "pkg:npm/unified-ble-manager@4.0.19",
10
10
  "name": "unified-ble-manager",
11
- "version": "4.0.17",
11
+ "version": "4.0.19",
12
12
  "licenses": [
13
13
  {
14
14
  "expression": "Apache-2.0"
15
15
  }
16
16
  ],
17
- "purl": "pkg:npm/unified-ble-manager@4.0.17"
17
+ "purl": "pkg:npm/unified-ble-manager@4.0.19"
18
18
  },
19
19
  "properties": [
20
20
  {
@@ -537,7 +537,7 @@
537
537
  "dependsOn": []
538
538
  },
539
539
  {
540
- "ref": "pkg:npm/unified-ble-manager@4.0.17",
540
+ "ref": "pkg:npm/unified-ble-manager@4.0.19",
541
541
  "dependsOn": [
542
542
  "pkg:npm/%40babel/runtime@7.29.7",
543
543
  "pkg:npm/node-addon-api@8.9.0",
@@ -4,7 +4,7 @@
4
4
  "schemaVersion": "1.0.0",
5
5
  "package": {
6
6
  "name": "unified-ble-manager",
7
- "version": "4.0.17"
7
+ "version": "4.0.19"
8
8
  },
9
9
  "source": {
10
10
  "method": "pnpm-lock production graph with installed-manifest license audit",
@@ -879,11 +879,10 @@ class UnifiedBleProtocolAndroidDispatcher(
879
879
  message: String,
880
880
  androidGattStatus: Int? = null
881
881
  ) {
882
- val bondedPeerCommand = command.requiredString(3) == "enumerateBondedPeers"
883
- if (bondedPeerCommand) {
884
- // Keep failure/cancellation/teardown mutually exclusive with a late success.
885
- if (!claimExactPendingCommand(pendingCommands, operationKey(command), command)) return
886
- } else if (!isPending(command)) return
882
+ // Claim before constructing or emitting the terminal. Link loss can race a
883
+ // native CCCD callback; a check followed by a later claim lets both paths
884
+ // publish a result for the same operation.
885
+ if (!claimExactPendingCommand(pendingCommands, operationKey(command), command)) return
887
886
  val errorFields = mutableMapOf<Int, ProtocolWireValue>(
888
887
  1 to ProtocolWireValue.StringValue(code),
889
888
  2 to ProtocolWireValue.StringValue("android"),
@@ -908,7 +907,6 @@ class UnifiedBleProtocolAndroidDispatcher(
908
907
  )
909
908
  )
910
909
  UnifiedBleProtocolJsiBinding.emitRecord(nativeHandle, ProtocolWireEncoder.encode(result))
911
- if (!bondedPeerCommand) claimExactPendingCommand(pendingCommands, operationKey(command), command)
912
910
  radioOperationIds.remove(operationKey(command))
913
911
  }
914
912
 
@@ -117,11 +117,11 @@ internal class OwnedAndroidSubscriptionOwnership<K> {
117
117
  */
118
118
  internal class AndroidGattOperationFailure(
119
119
  val operation: String,
120
- val gattStatus: Int
121
- ) : IllegalStateException("$operation status=$gattStatus") {
122
- val isLinkLoss: Boolean
123
- get() = gattStatus == ANDROID_GATT_LINK_LOSS_STATUS
124
- }
120
+ val gattStatus: Int?,
121
+ val isLinkLoss: Boolean = gattStatus == ANDROID_GATT_LINK_LOSS_STATUS
122
+ ) : IllegalStateException(
123
+ if (gattStatus == null) "$operation failed because the GATT link is unavailable" else "$operation status=$gattStatus"
124
+ )
125
125
 
126
126
  internal const val ANDROID_GATT_LINK_LOSS_STATUS = 19
127
127
 
@@ -130,6 +130,15 @@ internal fun classifyAndroidGattOperationFailure(
130
130
  gattStatus: Int
131
131
  ): AndroidGattOperationFailure = AndroidGattOperationFailure(operation, gattStatus)
132
132
 
133
+ internal fun classifyAndroidNotificationRegistrationFailure(
134
+ operation: String,
135
+ connected: Boolean
136
+ ): Throwable = if (connected) {
137
+ IllegalStateException("$operation failed")
138
+ } else {
139
+ AndroidGattOperationFailure(operation, null, isLinkLoss = true)
140
+ }
141
+
133
142
  /** Runtime adapter state derived from Android hardware and granted permissions. */
134
143
  internal data class OwnedRadioAdapterProtocolState(
135
144
  val availability: String,
@@ -1556,7 +1565,15 @@ class OwnedAndroidGattRadio(private val context: Context) {
1556
1565
  return@enqueue
1557
1566
  }
1558
1567
  if (!gatt.setCharacteristicNotification(ch, enable)) {
1559
- onResult(Result.failure(IllegalStateException("setCharacteristicNotification failed")))
1568
+ val connected = runCatching { isConnected(deviceId) }.getOrDefault(true)
1569
+ onResult(
1570
+ Result.failure(
1571
+ classifyAndroidNotificationRegistrationFailure(
1572
+ "setCharacteristicNotification",
1573
+ connected
1574
+ )
1575
+ )
1576
+ )
1560
1577
  done()
1561
1578
  return@enqueue
1562
1579
  }
@@ -1666,10 +1683,16 @@ class OwnedAndroidGattRadio(private val context: Context) {
1666
1683
  return@enqueue
1667
1684
  }
1668
1685
  if (!gatt.setCharacteristicNotification(characteristic, enable)) {
1686
+ val connected = runCatching { isConnected(deviceId) }.getOrDefault(true)
1669
1687
  completeExactUnitDirect(
1670
1688
  onResult,
1671
1689
  done,
1672
- Result.failure(IllegalStateException("setCharacteristicNotification failed")),
1690
+ Result.failure(
1691
+ classifyAndroidNotificationRegistrationFailure(
1692
+ "setCharacteristicNotification",
1693
+ connected
1694
+ )
1695
+ ),
1673
1696
  token
1674
1697
  )
1675
1698
  return@enqueue
@@ -13,6 +13,7 @@ import com.sfourdrinier.unifiedblemanager.radio.normalizeBondedPeerSnapshots
13
13
  import com.sfourdrinier.unifiedblemanager.radio.bondedPeerAdapterReadiness
14
14
  import com.sfourdrinier.unifiedblemanager.radio.requiresImmediateGattTeardownOnAdapterState
15
15
  import com.sfourdrinier.unifiedblemanager.radio.classifyAndroidGattOperationFailure
16
+ import com.sfourdrinier.unifiedblemanager.radio.classifyAndroidNotificationRegistrationFailure
16
17
  import org.junit.Assert.assertEquals
17
18
  import org.junit.Assert.assertArrayEquals
18
19
  import org.junit.Assert.assertFalse
@@ -62,6 +63,23 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
62
63
  assertNull(androidGattOperationFailureStatus(IllegalStateException("onDescriptorWrite status=19")))
63
64
  }
64
65
 
66
+ @Test
67
+ fun synchronousNotificationRegistrationFailureIsTypedAsLinkLossWhenAndroidReportsDisconnected() {
68
+ val disconnected = classifyAndroidNotificationRegistrationFailure(
69
+ operation = "notification-registration",
70
+ connected = false
71
+ )
72
+ assertEquals("connectionLost", androidGattOperationFailureCode(disconnected, "subscriptionFailed"))
73
+ assertNull(androidGattOperationFailureStatus(disconnected))
74
+
75
+ val connected = classifyAndroidNotificationRegistrationFailure(
76
+ operation = "notification-registration",
77
+ connected = true
78
+ )
79
+ assertEquals("subscriptionFailed", androidGattOperationFailureCode(connected, "subscriptionFailed"))
80
+ assertNull(androidGattOperationFailureStatus(connected))
81
+ }
82
+
65
83
  @Test
66
84
  fun bondedPeerAdapterReadinessReturnsTypedFailuresInsteadOfAnEmptySuccess() {
67
85
  assertTrue(
@@ -269,7 +287,6 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
269
287
  dispatcher.indexOf("private fun emitFailure"),
270
288
  dispatcher.indexOf("private fun emitCancelled")
271
289
  )
272
- assertTrue(failure.contains("bondedPeerCommand"))
273
290
  assertTrue(
274
291
  failure.indexOf("claimExactPendingCommand") <
275
292
  failure.indexOf("UnifiedBleProtocolJsiBinding.emitRecord")
@@ -281,6 +298,28 @@ class UnifiedBleProtocolAndroidDispatcherLifecycleTest {
281
298
  )
282
299
  }
283
300
 
301
+ @Test
302
+ fun cccdFailureClaimsThePendingCommandBeforeEmittingItsTerminal() {
303
+ val dispatcher = readAndroidSource(
304
+ "android/src/main/java/com/sfourdrinier/unifiedblemanager/protocol/UnifiedBleProtocolAndroidDispatcher.kt"
305
+ )
306
+ val failure = dispatcher.substring(
307
+ dispatcher.indexOf("private fun emitFailure"),
308
+ dispatcher.indexOf("private fun emitCancelled")
309
+ )
310
+
311
+ // Link loss and the CCCD callback can run concurrently. Claiming before
312
+ // emission makes the first terminal the owner; a late subscriptionFailed
313
+ // callback must not pass a check-then-emit race and publish a duplicate.
314
+ val claimIndex = failure.indexOf(
315
+ "if (!claimExactPendingCommand(pendingCommands, operationKey(command), command)) return",
316
+ failure.indexOf("Claim before constructing")
317
+ )
318
+ val emitIndex = failure.indexOf("UnifiedBleProtocolJsiBinding.emitRecord")
319
+ assertTrue(claimIndex >= 0)
320
+ assertTrue(claimIndex < emitIndex)
321
+ }
322
+
284
323
  @Test
285
324
  fun bondedPeerSnapshotBridgeHasAnOwnedRadioAndDispatcherRoute() {
286
325
  val radio = readAndroidSource(
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.17`. 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.19`. 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.17` source. Those options match
10
+ Use the v2 plugin options in this `4.0.19` 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.17`; the npm registry and release
17
+ This source targets `unified-ble-manager@4.0.19`; 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.17`; verify the published version in the npm registry.
7
+ This source targets `4.0.19`; 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.17`, install that exact version and keep it in
41
+ After the npm registry lists `4.0.19`, 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.17
45
+ pnpm add unified-ble-manager@4.0.19
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.17`. 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.19`. 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.17`. 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.19`. 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.17`
9
+ `unified-ble-manager@4.0.19`
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
 
@@ -1400,6 +1400,11 @@ void dispatchCommand(
1400
1400
  const auto subscription = requiredString(command, 7U);
1401
1401
  const auto subscriptionIdentifier = [NSString stringWithUTF8String:subscription.c_str()];
1402
1402
  void (^completion)(NSError*) = ^(NSError* error) {
1403
+ // CoreBluetooth can complete a CCCD write after didDisconnect. The
1404
+ // disconnect path removes this connection generation before publishing
1405
+ // its event, so suppress any completion that is now stale and let the
1406
+ // boundary's connectionLost terminal own the pending operation.
1407
+ if (!currentConnectionGenerationMatches(state, endpoint.peer, endpoint.connectionGeneration)) return;
1403
1408
  if (error == nil) static_cast<void>(success(state, command));
1404
1409
  else fail(state, command, "subscriptionFailed", error);
1405
1410
  };
@@ -7,5 +7,5 @@ exports.UNIFIED_BLE_IMPLEMENTATION_VERSION = void 0;
7
7
  // src/implementation-version.ts
8
8
 
9
9
  /** The one production version stamped into every first-party backend identity. */
10
- const UNIFIED_BLE_IMPLEMENTATION_VERSION = exports.UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.17';
10
+ const UNIFIED_BLE_IMPLEMENTATION_VERSION = exports.UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.19';
11
11
  //# sourceMappingURL=implementation-version.js.map
@@ -3,5 +3,5 @@
3
3
  // src/implementation-version.ts
4
4
 
5
5
  /** The one production version stamped into every first-party backend identity. */
6
- export const UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.17';
6
+ export const UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.19';
7
7
  //# sourceMappingURL=implementation-version.js.map
@@ -4,7 +4,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
4
4
  import { type BluezBusKind, type BluezDbusBoundaryFactory } from './bluez-dbus-contract';
5
5
  export declare const BLUEZ_BACKEND_ID = "unified-ble:bluez-dbus";
6
6
  export declare const BLUEZ_PLATFORM_ID = "unified-ble:linux-bluez";
7
- export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.17";
7
+ export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.19";
8
8
  export declare const bluezCompatibility: BackendCompatibilityOffer;
9
9
  export interface BluezBackendProviderOptions {
10
10
  readonly busKind: BluezBusKind;
@@ -1,7 +1,7 @@
1
1
  import { type FeatureRegistry } from '../../backend-contract/capabilities';
2
2
  export declare const COREBLUETOOTH_BACKEND_ID = "unified-ble:corebluetooth";
3
3
  export declare const COREBLUETOOTH_PLATFORM_ID = "unified-ble:macos-corebluetooth";
4
- export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.17";
4
+ export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.19";
5
5
  /** Identity metadata for a direct-GATT boundary that shares this backend core. */
6
6
  export interface DirectGattBackendIdentityOptions {
7
7
  readonly registeredBackendId: string;
@@ -9,7 +9,7 @@ import { ReactNativeAndroidProtocolBoundary } from '../../native-protocol/rn-and
9
9
  import { ReactNativeRestorationCoordinator, type ReactNativeRestorationActivation, type ReactNativeRestorationBackendProvider } from './react-native-restoration';
10
10
  export declare const REACT_NATIVE_ANDROID_BACKEND_ID = "unified-ble:react-native-android";
11
11
  export declare const REACT_NATIVE_ANDROID_PLATFORM_ID = "unified-ble:android-gatt";
12
- export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.17";
12
+ export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.19";
13
13
  export declare const REACT_NATIVE_ANDROID_DEFAULT_ADAPTER_NATIVE_ID = "android-default-adapter";
14
14
  export declare const reactNativeAndroidCompatibility: NativeCompatibilityOffer;
15
15
  export interface ReactNativeAndroidBackendProviderOptions {
@@ -3,7 +3,7 @@ import type { Spec as NativeProtocolControl } from '../../NativeUnifiedBleProtoc
3
3
  import { type ReactNativeRestorationBackendProvider } from './react-native-restoration';
4
4
  export declare const REACT_NATIVE_APPLE_BACKEND_ID = "unified-ble:react-native-apple";
5
5
  export declare const REACT_NATIVE_APPLE_PLATFORM_ID = "unified-ble:apple-corebluetooth";
6
- export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.17";
6
+ export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.19";
7
7
  export declare const REACT_NATIVE_APPLE_DEFAULT_ADAPTER_NATIVE_ID = "apple-corebluetooth-default-adapter";
8
8
  export declare const reactNativeAppleCompatibility: NativeCompatibilityOffer;
9
9
  export interface ReactNativeAppleBackendProviderOptions {
@@ -3,7 +3,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
3
3
  import { type WinRtAdapterRecord, type WinRtBoundary } from './winrt-boundary';
4
4
  export declare const WINRT_BACKEND_ID = "unified-ble:winrt";
5
5
  export declare const WINRT_PLATFORM_ID = "unified-ble:windows-winrt";
6
- export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.17";
6
+ export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.19";
7
7
  export declare const winRtCompatibility: BackendCompatibilityOffer;
8
8
  export interface WinRtBackendProviderOptions {
9
9
  readonly boundaryFactory: () => WinRtBoundary;
@@ -1,3 +1,3 @@
1
1
  /** The one production version stamped into every first-party backend identity. */
2
- export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.17";
2
+ export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.19";
3
3
  //# sourceMappingURL=implementation-version.d.ts.map
@@ -4,7 +4,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
4
4
  import { type BluezBusKind, type BluezDbusBoundaryFactory } from './bluez-dbus-contract.js';
5
5
  export declare const BLUEZ_BACKEND_ID = "unified-ble:bluez-dbus";
6
6
  export declare const BLUEZ_PLATFORM_ID = "unified-ble:linux-bluez";
7
- export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.17";
7
+ export declare const BLUEZ_IMPLEMENTATION_VERSION = "4.0.19";
8
8
  export declare const bluezCompatibility: BackendCompatibilityOffer;
9
9
  export interface BluezBackendProviderOptions {
10
10
  readonly busKind: BluezBusKind;
@@ -1,7 +1,7 @@
1
1
  import { type FeatureRegistry } from '../../backend-contract/capabilities.js';
2
2
  export declare const COREBLUETOOTH_BACKEND_ID = "unified-ble:corebluetooth";
3
3
  export declare const COREBLUETOOTH_PLATFORM_ID = "unified-ble:macos-corebluetooth";
4
- export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.17";
4
+ export declare const COREBLUETOOTH_IMPLEMENTATION_VERSION = "4.0.19";
5
5
  /** Identity metadata for a direct-GATT boundary that shares this backend core. */
6
6
  export interface DirectGattBackendIdentityOptions {
7
7
  readonly registeredBackendId: string;
@@ -9,7 +9,7 @@ import { ReactNativeAndroidProtocolBoundary } from '../../native-protocol/rn-and
9
9
  import { ReactNativeRestorationCoordinator, type ReactNativeRestorationActivation, type ReactNativeRestorationBackendProvider } from './react-native-restoration.js';
10
10
  export declare const REACT_NATIVE_ANDROID_BACKEND_ID = "unified-ble:react-native-android";
11
11
  export declare const REACT_NATIVE_ANDROID_PLATFORM_ID = "unified-ble:android-gatt";
12
- export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.17";
12
+ export declare const REACT_NATIVE_ANDROID_IMPLEMENTATION_VERSION = "4.0.19";
13
13
  export declare const REACT_NATIVE_ANDROID_DEFAULT_ADAPTER_NATIVE_ID = "android-default-adapter";
14
14
  export declare const reactNativeAndroidCompatibility: NativeCompatibilityOffer;
15
15
  export interface ReactNativeAndroidBackendProviderOptions {
@@ -3,7 +3,7 @@ import type { Spec as NativeProtocolControl } from '../../NativeUnifiedBleProtoc
3
3
  import { type ReactNativeRestorationBackendProvider } from './react-native-restoration.js';
4
4
  export declare const REACT_NATIVE_APPLE_BACKEND_ID = "unified-ble:react-native-apple";
5
5
  export declare const REACT_NATIVE_APPLE_PLATFORM_ID = "unified-ble:apple-corebluetooth";
6
- export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.17";
6
+ export declare const REACT_NATIVE_APPLE_IMPLEMENTATION_VERSION = "4.0.19";
7
7
  export declare const REACT_NATIVE_APPLE_DEFAULT_ADAPTER_NATIVE_ID = "apple-corebluetooth-default-adapter";
8
8
  export declare const reactNativeAppleCompatibility: NativeCompatibilityOffer;
9
9
  export interface ReactNativeAppleBackendProviderOptions {
@@ -3,7 +3,7 @@ import { type BackendCompatibilityOffer } from '../../backend-contract/primitive
3
3
  import { type WinRtAdapterRecord, type WinRtBoundary } from './winrt-boundary.js';
4
4
  export declare const WINRT_BACKEND_ID = "unified-ble:winrt";
5
5
  export declare const WINRT_PLATFORM_ID = "unified-ble:windows-winrt";
6
- export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.17";
6
+ export declare const WINRT_IMPLEMENTATION_VERSION = "4.0.19";
7
7
  export declare const winRtCompatibility: BackendCompatibilityOffer;
8
8
  export interface WinRtBackendProviderOptions {
9
9
  readonly boundaryFactory: () => WinRtBoundary;
@@ -1,3 +1,3 @@
1
1
  /** The one production version stamped into every first-party backend identity. */
2
- export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.17";
2
+ export declare const UNIFIED_BLE_IMPLEMENTATION_VERSION = "4.0.19";
3
3
  //# sourceMappingURL=implementation-version.d.ts.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "package": "unified-ble-manager@4.0.17",
3
+ "package": "unified-ble-manager@4.0.19",
4
4
  "nodeApiVersion": 8,
5
5
  "entries": [
6
6
  {
@@ -10,7 +10,7 @@
10
10
  "nodeApiVersion": 8,
11
11
  "path": "native/electron/corebluetooth/prebuilds/darwin-arm64/unified_ble_corebluetooth.node",
12
12
  "bytes": 332368,
13
- "sha256": "94bfdc1d203952850cce685d0450152e34fdfbb367c8190e851c2701ea6d8732"
13
+ "sha256": "61b75e5654408cdd18ac9697b620676b460a58f8c209fe8e3ea784f22ad715d5"
14
14
  },
15
15
  {
16
16
  "backend": "corebluetooth",
@@ -19,7 +19,7 @@
19
19
  "nodeApiVersion": 8,
20
20
  "path": "native/electron/corebluetooth/prebuilds/darwin-x64/unified_ble_corebluetooth.node",
21
21
  "bytes": 314872,
22
- "sha256": "cb4e8921ff26a0e97592b7ca869469128136b18a703d8e5b2d88aca1621dec62"
22
+ "sha256": "663ab35a6abfc38481d90c21f05f51e473675871c4abf89204785dfe8b810a72"
23
23
  },
24
24
  {
25
25
  "backend": "winrt",
@@ -28,7 +28,7 @@
28
28
  "nodeApiVersion": 8,
29
29
  "path": "native/electron/winrt/prebuilds/win32-arm64/unified_ble_winrt.node",
30
30
  "bytes": 644608,
31
- "sha256": "da46e3109cafed76e571c9733cb7a3ed41596c562d704f539390bca1330eeda7"
31
+ "sha256": "f0c87192e08ba429a069ea8d40bdc04926ae58d7f0212b8afd29de72c9a21773"
32
32
  },
33
33
  {
34
34
  "backend": "winrt",
@@ -37,7 +37,7 @@
37
37
  "nodeApiVersion": 8,
38
38
  "path": "native/electron/winrt/prebuilds/win32-x64/unified_ble_winrt.node",
39
39
  "bytes": 614400,
40
- "sha256": "64ec59e5c415f6ab8446374509db7ea3dfe0bd06f8c3291faebb457e073818b3"
40
+ "sha256": "2f3143d4e784d768fa603a01e68e3229fc8e84ee31d736d138e745b3cc4295c7"
41
41
  }
42
42
  ]
43
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unified-ble-manager",
3
- "version": "4.0.17",
3
+ "version": "4.0.19",
4
4
  "description": "Unified Bluetooth Low Energy manager for React Native, Electron, Tauri, Web, and desktop",
5
5
  "packageManager": "pnpm@10.14.0",
6
6
  "main": "lib/commonjs/index.js",
@@ -1,4 +1,4 @@
1
1
  // src/implementation-version.ts
2
2
 
3
3
  /** The one production version stamped into every first-party backend identity. */
4
- export const UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.17'
4
+ export const UNIFIED_BLE_IMPLEMENTATION_VERSION = '4.0.19'