unified-ble-manager 4.0.0-alpha.21 → 4.0.0-alpha.24

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 (64) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/MIGRATION_4.0.md +4 -4
  3. package/README.md +8 -8
  4. package/RELEASE.md +11 -11
  5. package/ROADMAP.4.0.md +2 -2
  6. package/docs/ELECTRON.md +2 -2
  7. package/docs/EXPO_PLUGIN.md +1 -1
  8. package/docs/GAPS.4.0.md +1 -1
  9. package/docs/GETTING_STARTED.md +3 -3
  10. package/docs/NODE.md +1 -1
  11. package/docs/PLATFORMS.md +2 -2
  12. package/docs/WEB.md +1 -1
  13. package/lib/commonjs/electron/main-binding.js +221 -3
  14. package/lib/commonjs/electron/main-binding.js.map +1 -1
  15. package/lib/commonjs/electron/main-router.js +24 -2
  16. package/lib/commonjs/electron/main-router.js.map +1 -1
  17. package/lib/commonjs/electron/protocol.js +2 -0
  18. package/lib/commonjs/electron/protocol.js.map +1 -1
  19. package/lib/commonjs/electron/renderer.js +53 -10
  20. package/lib/commonjs/electron/renderer.js.map +1 -1
  21. package/lib/commonjs/implementation-version.js +1 -1
  22. package/lib/module/electron/main-binding.js +222 -4
  23. package/lib/module/electron/main-binding.js.map +1 -1
  24. package/lib/module/electron/main-router.js +24 -2
  25. package/lib/module/electron/main-router.js.map +1 -1
  26. package/lib/module/electron/protocol.js +2 -0
  27. package/lib/module/electron/protocol.js.map +1 -1
  28. package/lib/module/electron/renderer.js +54 -11
  29. package/lib/module/electron/renderer.js.map +1 -1
  30. package/lib/module/implementation-version.js +1 -1
  31. package/lib/typescript/commonjs/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
  32. package/lib/typescript/commonjs/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  33. package/lib/typescript/commonjs/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
  34. package/lib/typescript/commonjs/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  35. package/lib/typescript/commonjs/src/backends/winrt/winrt-provider.d.ts +1 -1
  36. package/lib/typescript/commonjs/src/electron/main-binding.d.ts +21 -2
  37. package/lib/typescript/commonjs/src/electron/main-binding.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/src/electron/main-router.d.ts +2 -2
  39. package/lib/typescript/commonjs/src/electron/main-router.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/src/electron/protocol.d.ts +9 -3
  41. package/lib/typescript/commonjs/src/electron/protocol.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/src/electron/renderer.d.ts +7 -0
  43. package/lib/typescript/commonjs/src/electron/renderer.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/src/implementation-version.d.ts +1 -1
  45. package/lib/typescript/module/src/backends/bluez/bluez-backend-provider.d.ts +1 -1
  46. package/lib/typescript/module/src/backends/corebluetooth/corebluetooth-identity.d.ts +1 -1
  47. package/lib/typescript/module/src/backends/reactnative/react-native-android-provider.d.ts +1 -1
  48. package/lib/typescript/module/src/backends/reactnative/react-native-apple-provider.d.ts +1 -1
  49. package/lib/typescript/module/src/backends/winrt/winrt-provider.d.ts +1 -1
  50. package/lib/typescript/module/src/electron/main-binding.d.ts +21 -2
  51. package/lib/typescript/module/src/electron/main-binding.d.ts.map +1 -1
  52. package/lib/typescript/module/src/electron/main-router.d.ts +2 -2
  53. package/lib/typescript/module/src/electron/main-router.d.ts.map +1 -1
  54. package/lib/typescript/module/src/electron/protocol.d.ts +9 -3
  55. package/lib/typescript/module/src/electron/protocol.d.ts.map +1 -1
  56. package/lib/typescript/module/src/electron/renderer.d.ts +7 -0
  57. package/lib/typescript/module/src/electron/renderer.d.ts.map +1 -1
  58. package/lib/typescript/module/src/implementation-version.d.ts +1 -1
  59. package/package.json +1 -1
  60. package/src/electron/main-binding.ts +318 -13
  61. package/src/electron/main-router.ts +21 -5
  62. package/src/electron/protocol.ts +16 -3
  63. package/src/electron/renderer.ts +64 -12
  64. package/src/implementation-version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,52 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [4.0.0-alpha.24] - 2026-07-31
10
+
11
+ ### Changed
12
+
13
+ - Electron event acknowledgements now preserve normalized contract failures
14
+ across the main/preload/renderer boundary instead of collapsing them into
15
+ generic transport errors.
16
+
17
+ ### Fixed
18
+
19
+ - Made renderer ownership self-healing after an Electron document reload or
20
+ replacement navigation: the exact stale lease is invalidated, its resources
21
+ are quarantined, and a fresh renderer client can bootstrap without reusing
22
+ scans, connections, subscriptions, or pending events from the old generation.
23
+ - Prevented permanent acknowledgement failures, oversized-response rollback
24
+ failures, malformed IPC input, and late renderer cleanup from leaking or
25
+ retrying resources indefinitely.
26
+ - Preserved the final authoritative main-router cleanup receipt when renderer
27
+ destruction follows an earlier ambiguous cleanup failure.
28
+
29
+ ## [4.0.0-alpha.23] - 2026-07-31
30
+
31
+ ### Fixed
32
+
33
+ - Updated the canonical packed-artifact Electron smoke to model the complete
34
+ Electron 43 navigation and load-failure listener surface used by renderer
35
+ ownership tracking.
36
+
37
+ ## [4.0.0-alpha.22] - 2026-07-31
38
+
39
+ ### Changed
40
+
41
+ - Electron renderer ownership now tracks replacement navigation with Electron's
42
+ structured navigation details and one sender-scoped navigation epoch shared
43
+ by overlapping renderer leases.
44
+ - Added compiler proof that Electron 43 `WebContents` satisfies the public
45
+ main-process binding contract.
46
+
47
+ ### Fixed
48
+
49
+ - Prevented the initial document's late `did-navigate` event from retiring the
50
+ renderer lease that document had just bootstrapped.
51
+ - Quarantined every lease admitted after a replacement navigation starts,
52
+ including Strict Mode overlap, so the committed replacement document cannot
53
+ inherit stale scans, connections, subscriptions, or acknowledgements.
54
+
9
55
  ## [4.0.0-alpha.21] - 2026-07-31
10
56
 
11
57
  ### Added
package/MIGRATION_4.0.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Architecture and sequencing authority:** [`docs/UNIFIED_BLE_4.0_IMPLEMENTATION_PLAN.md`](docs/UNIFIED_BLE_4.0_IMPLEMENTATION_PLAN.md)
8
8
 
9
- `unified-ble-manager@4.0.0-alpha.21` is the current public 4.0 prerelease. 4.0
9
+ `unified-ble-manager@4.0.0-alpha.24` is the current public 4.0 prerelease. 4.0
10
10
  started as a new package with no released 4.0 consumer baseline. This is an
11
11
  adoption guide for a project that chooses to move from another BLE integration,
12
12
  not a supported in-place upgrade from `@sfourdrinier/react-native-ble-plx`.
@@ -18,13 +18,13 @@ Install the exact currently supported package line with your application's
18
18
  package manager:
19
19
 
20
20
  ```sh
21
- pnpm add unified-ble-manager@4.0.0-alpha.21
21
+ pnpm add unified-ble-manager@4.0.0-alpha.24
22
22
  ```
23
23
 
24
- `next` is the mutable prerelease dist-tag; it currently resolves to alpha.21.
24
+ `next` is the mutable prerelease dist-tag; it currently resolves to alpha.24.
25
25
  Pin the exact version you evaluate, and do not use a bare install or `@latest`
26
26
  to select the 4.0 alpha train. The prerelease package is Experimental: no
27
- current evidence record links alpha.21's package artifact to a hardware-backed
27
+ current evidence record links alpha.24's package artifact to a hardware-backed
28
28
  backend support claim. See [`docs/PLATFORMS.md`](docs/PLATFORMS.md).
29
29
 
30
30
  Use only documented package subpaths. The root export is host-neutral; import
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # unified-ble-manager
4
4
 
5
- `unified-ble-manager@4.0.0-alpha.21` is the current public 4.0 prerelease for
5
+ `unified-ble-manager@4.0.0-alpha.24` is the current public 4.0 prerelease for
6
6
  explicit, bytes-first Bluetooth Low Energy management across React Native, Web,
7
7
  Electron, and Node hosts. 4.0 is a new package line with no released 4.0
8
8
  consumer baseline; adopting it is an explicit integration, not a source-
@@ -16,14 +16,14 @@ The controlling architecture and sequencing record is
16
16
 
17
17
  ## Release status and evidence boundary
18
18
 
19
- `v4.0.0-alpha.21` is published to npm under the `next` dist-tag and has a
20
- [GitHub Release](https://github.com/sfourdrinier/react-native-ble-plx/releases/tag/v4.0.0-alpha.21).
19
+ `v4.0.0-alpha.24` is published to npm under the `next` dist-tag and has a
20
+ [GitHub Release](https://github.com/sfourdrinier/react-native-ble-plx/releases/tag/v4.0.0-alpha.24).
21
21
  It was published by GitHub Actions trusted publishing with an npm SLSA
22
22
  provenance attestation. This proves the package release path, not Bluetooth
23
23
  behavior.
24
24
 
25
25
  The package and deterministic contract surface are **Experimental**. No current
26
- evidence record binds the published alpha.21 artifact to a hardware-backed
26
+ evidence record binds the published alpha.24 artifact to a hardware-backed
27
27
  backend scenario, so no React Native, Web, Node, or Electron backend is
28
28
  Preview, Live Preview, Supported, or Reliability-qualified. Missing hardware
29
29
  evidence blocks those labels only; it does not invalidate deterministic or
@@ -47,10 +47,10 @@ validation substitute.
47
47
  Pin the exact prerelease you validated. pnpm is used in this repository:
48
48
 
49
49
  ```sh
50
- pnpm add unified-ble-manager@4.0.0-alpha.21
50
+ pnpm add unified-ble-manager@4.0.0-alpha.24
51
51
  ```
52
52
 
53
- `next` is the mutable prerelease dist-tag. It currently resolves to alpha.21,
53
+ `next` is the mutable prerelease dist-tag. It currently resolves to alpha.24,
54
54
  but a later alpha can change it without changing your lockfile intent. Do not install
55
55
  the bare package name or `@latest` when adopting 4.0 alpha: those do not select
56
56
  the 4.0 prerelease train. Use your package manager's exact-version syntax when
@@ -300,10 +300,10 @@ final version uses npm `latest` and a normal GitHub Release.
300
300
  Check an installed-release candidate against npm metadata before integrating it:
301
301
 
302
302
  ```sh
303
- npm view unified-ble-manager@4.0.0-alpha.21 version dist-tags dist.integrity dist.attestations _npmUser --json
303
+ npm view unified-ble-manager@4.0.0-alpha.24 version dist-tags dist.integrity dist.attestations _npmUser --json
304
304
  ```
305
305
 
306
- For alpha.21, the result must identify the exact version, `next` dist-tag,
306
+ For alpha.24, the result must identify the exact version, `next` dist-tag,
307
307
  package integrity, a SLSA provenance attestation, and GitHub Actions as the npm
308
308
  trusted publisher. Cross-check the tag and release notes at the GitHub Release
309
309
  linked above. Provenance and integrity do not verify live Bluetooth behavior;
package/RELEASE.md CHANGED
@@ -20,18 +20,18 @@ especially its package, evidence, deletion, and Section 31 release gates.
20
20
 
21
21
  ## Current public prerelease
22
22
 
23
- `unified-ble-manager@4.0.0-alpha.21` was published from
24
- `v4.0.0-alpha.21` by GitHub Actions trusted publishing. The published npm
23
+ `unified-ble-manager@4.0.0-alpha.24` was published from
24
+ `v4.0.0-alpha.24` by GitHub Actions trusted publishing. The published npm
25
25
  metadata identifies GitHub Actions as the trusted publisher and includes an npm
26
26
  SLSA provenance attestation. Its
27
- [GitHub Release](https://github.com/sfourdrinier/react-native-ble-plx/releases/tag/v4.0.0-alpha.21)
28
- is a prerelease and its notes are generated from the alpha.21 section of
27
+ [GitHub Release](https://github.com/sfourdrinier/react-native-ble-plx/releases/tag/v4.0.0-alpha.24)
28
+ is a prerelease and its notes are generated from the alpha.24 section of
29
29
  [`CHANGELOG.md`](CHANGELOG.md).
30
30
 
31
31
  This is an Experimental package release. It proves a public package, tested
32
32
  exports, and the workflow's deterministic/package gates; it does not establish
33
33
  hardware support for a backend. The repository's current evidence records do
34
- not bind alpha.21's package artifact to a passed physical-radio scenario. No
34
+ not bind alpha.24's package artifact to a passed physical-radio scenario. No
35
35
  backend is thereby Preview, Live Preview, Supported, or Reliability-qualified.
36
36
  See [`docs/PLATFORMS.md`](docs/PLATFORMS.md) and
37
37
  [`evidence/v1/README.md`](evidence/v1/README.md).
@@ -47,7 +47,7 @@ verifying that the pushed `vX.Y.Z` tag exactly matches `package.json`:
47
47
 
48
48
  | Version form | npm dist-tag | GitHub Release state | Consumer guidance |
49
49
  | --- | --- | --- | --- |
50
- | Hyphenated SemVer prerelease, such as `4.0.0-alpha.21` | `next` | prerelease | Pin the exact version for reproducible evaluation; `@next` is mutable. |
50
+ | Hyphenated SemVer prerelease, such as `4.0.0-alpha.24` | `next` | prerelease | Pin the exact version for reproducible evaluation; `@next` is mutable. |
51
51
  | Final SemVer version, such as `4.0.0` | `latest` | normal release | Use the final version only after its published evidence supports the required host claim. |
52
52
 
53
53
  Do not use a bare install or `@latest` to select a 4.0 alpha. The exact package
@@ -87,18 +87,18 @@ Check the published version, dist-tag, integrity, attestation, and trusted
87
87
  publisher from npm:
88
88
 
89
89
  ```sh
90
- npm view unified-ble-manager@4.0.0-alpha.21 version dist-tags dist.integrity dist.attestations _npmUser --json
90
+ npm view unified-ble-manager@4.0.0-alpha.24 version dist-tags dist.integrity dist.attestations _npmUser --json
91
91
  ```
92
92
 
93
93
  Then cross-check the matching tag and GitHub Release:
94
94
 
95
95
  ```sh
96
- gh release view v4.0.0-alpha.21 --repo sfourdrinier/react-native-ble-plx --json tagName,isPrerelease,publishedAt,url
96
+ gh release view v4.0.0-alpha.24 --repo sfourdrinier/react-native-ble-plx --json tagName,isPrerelease,publishedAt,url
97
97
  ```
98
98
 
99
- For alpha.21, npm must report the exact version, `next`, integrity, a SLSA
99
+ For alpha.24, npm must report the exact version, `next`, integrity, a SLSA
100
100
  provenance attestation, and GitHub Actions trusted publisher; GitHub must report
101
- the matching `v4.0.0-alpha.21` tag with `isPrerelease: true`. These checks
101
+ the matching `v4.0.0-alpha.24` tag with `isPrerelease: true`. These checks
102
102
  verify release identity and supply chain metadata only. They do not verify BLE
103
103
  hardware behavior, platform permissions, browser availability, background
104
104
  operation, restoration, reconnect, or reliability.
@@ -111,7 +111,7 @@ backend, hardware, Live Preview, Supported, or Reliability-qualified claims.
111
111
  Deterministic fault injection remains useful 4.0 contract proof but is never
112
112
  physical-radio proof.
113
113
 
114
- At the time of alpha.21, GitHub private vulnerability reporting is disabled and
114
+ At the time of alpha.24, GitHub private vulnerability reporting is disabled and
115
115
  no private reporting channel or supported-version response policy is published.
116
116
  Release notes and support material must not claim otherwise. Establish and
117
117
  publish that external repository policy before advertising a confidential
package/ROADMAP.4.0.md CHANGED
@@ -44,7 +44,7 @@ Applications and vendor libraries own device choice, vendor protocols, product r
44
44
 
45
45
  ## Published alpha and support claims
46
46
 
47
- `unified-ble-manager@4.0.0-alpha.20` is published under npm's `next` dist-tag
47
+ `unified-ble-manager@4.0.0-alpha.24` is published under npm's `next` dist-tag
48
48
  with GitHub Actions trusted publishing, npm SLSA provenance, and a GitHub
49
49
  prerelease. Its host-neutral root and explicit public subpaths are the current
50
50
  alpha package contract; users must pin an exact alpha version rather than infer
@@ -52,7 +52,7 @@ an API from transitional source files or examples. The alpha has no 3.x
52
52
  compatibility layer.
53
53
 
54
54
  The current package release is Experimental. There is no current evidence record
55
- linking alpha.20's package artifact to a passed physical-radio backend scenario,
55
+ linking alpha.24's package artifact to a passed physical-radio backend scenario,
56
56
  so it makes no Preview-or-higher platform claim. Hardware evidence is required
57
57
  only for the corresponding support label; it does not erase package or
58
58
  deterministic proof.
package/docs/ELECTRON.md CHANGED
@@ -19,10 +19,10 @@ There is no Noble dependency, renderer Web Bluetooth fallback, legacy
19
19
  `BlePort`, `PortBleManager`, or mock-radio production fallback in these
20
20
  entrypoints.
21
21
 
22
- The published `4.0.0-alpha.21` Electron surface is Experimental. The release
22
+ The published `4.0.0-alpha.24` Electron surface is Experimental. The release
23
23
  workflow's packed Electron smoke is deterministic L1 package/IPC proof, not an
24
24
  Electron host, adapter, or peripheral support claim. No current evidence record
25
- binds alpha.21 to a physical Electron radio journey.
25
+ binds alpha.24 to a physical Electron radio journey.
26
26
 
27
27
  ## Main-process backend selection
28
28
 
@@ -12,7 +12,7 @@ options are exactly the schema implemented in `plugin/src/withBLE.ts`:
12
12
 
13
13
  The plugin configuration is part of the Experimental 4.0 alpha package. The
14
14
  release workflow validates Expo SDK 57 CNG prebuild and Android assembly, but no
15
- current evidence record binds alpha.21 to a physical device, permission,
15
+ current evidence record binds alpha.24 to a physical device, permission,
16
16
  background, or restoration scenario. Configuration and compilation therefore do
17
17
  not make a live-radio or restoration support claim.
18
18
 
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 pre-4.0 source tree contains a transitional `BleManager`, `BlePort`, `PortBleManager`, Base64 bridge, byte convenience methods, static capability helpers, Noble-era code, and host examples. Any statement below about that source is current-state characterization and migration input. It is not proof that a clean-baseline 4.0 replacement exists.
16
16
 
17
- `unified-ble-manager@4.0.0-alpha.21` is published as an Experimental package,
17
+ `unified-ble-manager@4.0.0-alpha.24` is published as an Experimental package,
18
18
  but no current record in `evidence/v1/records/` binds that artifact to a passed
19
19
  physical-radio backend scenario. The records below are historical L0 context and
20
20
  must not be read as Preview-or-higher evidence for the published alpha.
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 4.0 status
6
6
 
7
- `unified-ble-manager@4.0.0-alpha.21` is the current public 4.0 alpha package
7
+ `unified-ble-manager@4.0.0-alpha.24` is the current public 4.0 alpha package
8
8
  line. It is a clean API line launched with no released 4.0 consumer baseline:
9
9
  choose one explicit host entrypoint and build the matching native integration
10
10
  before making a Bluetooth claim. It is not a source-compatible rename of the
@@ -15,13 +15,13 @@ The architecture and implementation sequence are controlled by [`UNIFIED_BLE_4.0
15
15
  ## Install and select a host
16
16
 
17
17
  ```sh
18
- pnpm add unified-ble-manager@4.0.0-alpha.21
18
+ pnpm add unified-ble-manager@4.0.0-alpha.24
19
19
  ```
20
20
 
21
21
  The alpha train publishes under npm's mutable `next` dist-tag. Pin the exact
22
22
  version you validate; do not use a bare install or `@latest` to select 4.0
23
23
  alpha. The package is Experimental, and no current evidence record binds the
24
- published alpha.21 artifact to a physical-radio backend result. Package builds
24
+ published alpha.24 artifact to a physical-radio backend result. Package builds
25
25
  and deterministic tests remain useful proof at their own scope, but do not
26
26
  create a platform support label.
27
27
 
package/docs/NODE.md CHANGED
@@ -6,7 +6,7 @@ The root `unified-ble-manager` entrypoint is host-neutral. Node applications
6
6
  must select an owned backend through an explicit subpath; a package import never
7
7
  chooses an adapter, enables a mock, or falls back to Noble.
8
8
 
9
- The Node host surfaces in `4.0.0-alpha.21` are Experimental. No current
9
+ The Node host surfaces in `4.0.0-alpha.24` are Experimental. No current
10
10
  evidence record binds the published alpha package artifact to a physical BlueZ,
11
11
  CoreBluetooth, or WinRT scenario, so these entrypoints are not Preview-or-higher
12
12
  support claims.
package/docs/PLATFORMS.md CHANGED
@@ -8,8 +8,8 @@ This page is an evidence index, not a static 4.0 capability matrix. In 4.0, an a
8
8
 
9
9
  ## Current alpha maturity
10
10
 
11
- `unified-ble-manager@4.0.0-alpha.21` is an **Experimental** package release.
12
- No current evidence record binds the published alpha.21 artifact to a passed
11
+ `unified-ble-manager@4.0.0-alpha.24` is an **Experimental** package release.
12
+ No current evidence record binds the published alpha.24 artifact to a passed
13
13
  physical-radio backend scenario. Consequently, no backend below is currently
14
14
  Preview, Live Preview, Supported, or Reliability-qualified. This is not a
15
15
  static 4.0 capability matrix: the table says what public surface and automated
package/docs/WEB.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  Web Bluetooth is a first-party 4.0 backend with browser chooser constraints
10
10
  represented as a typed feature. It is not continuous scanning in disguise and
11
- does not grant a static support claim. `unified-ble-manager@4.0.0-alpha.21`
11
+ does not grant a static support claim. `unified-ble-manager@4.0.0-alpha.24`
12
12
  publishes this API under npm's `next` tag, but no current evidence record binds
13
13
  that artifact to a browser/adapter/peripheral live-radio scenario. Web is
14
14
  therefore Experimental, not Preview-or-higher.
@@ -30,6 +30,7 @@ const destroyedRendererRetryDelayMilliseconds = 100;
30
30
  class ElectronMainBleBinding {
31
31
  renderers = new Map();
32
32
  bootstrapAdmissionTails = new Map();
33
+ navigationStates = new WeakMap();
33
34
  installed = false;
34
35
  lifecycle = 'active';
35
36
  destroyResult = null;
@@ -42,7 +43,7 @@ class ElectronMainBleBinding {
42
43
  if (this.installed) {
43
44
  return;
44
45
  }
45
- this.options.port.handle(_protocol.ELECTRON_BLE_IPC_CHANNEL, (event, request) => this.handle(event, request));
46
+ this.options.port.handle(_protocol.ELECTRON_BLE_IPC_CHANNEL, (event, request) => this.handleIpcRequest(event, request));
46
47
  this.installed = true;
47
48
  }
48
49
  uninstall() {
@@ -123,6 +124,12 @@ class ElectronMainBleBinding {
123
124
  this.completeRendererRelease(rendererLeaseId);
124
125
  }
125
126
  }
127
+ if (routerCleanup.state === 'released') {
128
+ return {
129
+ state: 'released',
130
+ failures: []
131
+ };
132
+ }
126
133
  const failures = [...routerCleanup.failures];
127
134
  for (const cleanup of releaseRecords) {
128
135
  failures.push(...cleanup.failures);
@@ -135,6 +142,38 @@ class ElectronMainBleBinding {
135
142
  failures
136
143
  };
137
144
  }
145
+ async handleIpcRequest(event, request) {
146
+ try {
147
+ assertElectronBleIpcRequest(request);
148
+ return await this.handle(event, request);
149
+ } catch (error) {
150
+ return this.failureResponse(error, request);
151
+ }
152
+ }
153
+ failureResponse(error, request) {
154
+ if (error instanceof _errors.BackendContractError) {
155
+ return {
156
+ kind: 'failure',
157
+ error: error.normalized
158
+ };
159
+ }
160
+ console.error('[ElectronMainBleBinding] IPC request failed:', {
161
+ functionName: 'ElectronMainBleBinding.handleIpcRequest',
162
+ operation: electronIpcRequestKind(request),
163
+ error
164
+ });
165
+ return {
166
+ kind: 'failure',
167
+ error: (0, _errors.contractError)('platform.failure', 'ipc', 'electron-main-binding.ipc-handler', {
168
+ domain: 'electron-ipc',
169
+ code: 'unexpected-handler-error',
170
+ safeMessage: 'The Electron main process could not complete the BLE request.',
171
+ metadata: {
172
+ requestKind: electronIpcRequestKind(request)
173
+ }
174
+ }).normalized
175
+ };
176
+ }
138
177
  async handle(event, request) {
139
178
  this.assertActiveLifecycle();
140
179
  this.assertMainFrame(event);
@@ -162,7 +201,19 @@ class ElectronMainBleBinding {
162
201
  kind: 'event.ack'
163
202
  };
164
203
  }
165
- const response = await this.options.router.dispatch(trusted, request);
204
+ let response;
205
+ try {
206
+ response = await this.options.router.dispatch(trusted, request);
207
+ } catch (error) {
208
+ if (isRollbackReleaseRequiredError(error) && bound !== undefined) {
209
+ bound.releaseRequired = true;
210
+ const cleanup = await this.releaseRendererAuthoritatively(rendererLeaseId, bound);
211
+ if (cleanup?.state === 'released') {
212
+ throw (0, _errors.contractError)('ownership.denied', 'ipc', 'electron-main-arbiter.renderer-registration');
213
+ }
214
+ }
215
+ throw error;
216
+ }
166
217
  if (response.kind === 'release' && response.cleanup.state === 'released') {
167
218
  if (request.kind !== 'release') {
168
219
  throw (0, _errors.contractError)('lifecycle.invariant-violation', 'ipc', 'electron-main-binding.release-response');
@@ -200,6 +251,9 @@ class ElectronMainBleBinding {
200
251
  this.assertMainFrame(event);
201
252
  this.assertTrustedSenderCurrent(event, trusted);
202
253
  this.assertBootstrapSender(event.sender, trusted);
254
+ const navigationState = this.navigationState(event.sender);
255
+ const navigationEpochAtAdmission = navigationState.epoch;
256
+ const replacementWasPendingForAdmittedFrame = navigationState.pendingReplacement && navigationState.sourceFrame?.processId === event.processId && navigationState.sourceFrame.routingId === event.frameId;
203
257
  const response = await this.options.router.dispatch(trusted, {
204
258
  kind: 'bootstrap'
205
259
  });
@@ -212,17 +266,104 @@ class ElectronMainBleBinding {
212
266
  const destroyedListener = () => {
213
267
  this.retireRenderer(rendererLeaseId, renderer, true, 'destroyed');
214
268
  };
269
+ let replacementNavigationStarted = replacementWasPendingForAdmittedFrame || navigationState.epoch !== navigationEpochAtAdmission;
270
+ const navigationStartListener = details => {
271
+ if (details.isMainFrame && !details.isSameDocument) {
272
+ if (navigationState.lastStartDetails !== details) {
273
+ if (navigationState.pendingReplacement && navigationState.lastStartDetails !== null) {
274
+ navigationState.supersededTargetUrls.push(navigationState.lastStartDetails.url);
275
+ }
276
+ navigationState.epoch += 1;
277
+ navigationState.pendingReplacement = true;
278
+ navigationState.lastStartDetails = details;
279
+ navigationState.sourceFrame = Object.freeze({
280
+ processId: renderer.frame.processId,
281
+ routingId: renderer.frame.routingId
282
+ });
283
+ }
284
+ replacementNavigationStarted = true;
285
+ }
286
+ };
287
+ const navigationRedirectListener = details => {
288
+ if (details.isMainFrame && !details.isSameDocument && navigationState.pendingReplacement) {
289
+ navigationState.lastStartDetails = details;
290
+ }
291
+ };
215
292
  const navigationListener = () => {
216
- this.retireRenderer(rendererLeaseId, renderer, false, 'navigation');
293
+ navigationState.pendingReplacement = false;
294
+ navigationState.lastStartDetails = null;
295
+ navigationState.supersededTargetUrls.length = 0;
296
+ navigationState.unmatchedProvisionalFailures.clear();
297
+ navigationState.lastProvisionalFailureEvent = null;
298
+ navigationState.lastLoadFailureEvent = null;
299
+ navigationState.sourceFrame = null;
300
+ if (replacementNavigationStarted) {
301
+ this.retireRenderer(rendererLeaseId, renderer, false, 'navigation');
302
+ }
303
+ };
304
+ const applyNavigationFailure = (validatedUrl, isMainFrame) => {
305
+ if (!isMainFrame || !navigationState.pendingReplacement) {
306
+ return;
307
+ }
308
+ const supersededTargetIndex = navigationState.supersededTargetUrls.indexOf(validatedUrl);
309
+ if (navigationState.lastStartDetails?.url !== validatedUrl || supersededTargetIndex >= 0) {
310
+ if (supersededTargetIndex >= 0) {
311
+ navigationState.supersededTargetUrls.splice(supersededTargetIndex, 1);
312
+ }
313
+ return;
314
+ }
315
+ navigationState.pendingReplacement = false;
316
+ navigationState.lastStartDetails = null;
317
+ navigationState.supersededTargetUrls.length = 0;
318
+ navigationState.unmatchedProvisionalFailures.clear();
319
+ navigationState.sourceFrame = null;
320
+ };
321
+ const navigationProvisionalFailureListener = (failureEvent, errorCode, _errorDescription, validatedUrl, isMainFrame, frameProcessId, frameRoutingId) => {
322
+ if (navigationState.lastProvisionalFailureEvent === failureEvent) {
323
+ return;
324
+ }
325
+ navigationState.lastProvisionalFailureEvent = failureEvent;
326
+ const fingerprint = navigationFailureFingerprint(errorCode, validatedUrl, isMainFrame, frameProcessId, frameRoutingId);
327
+ const unmatchedCount = navigationState.unmatchedProvisionalFailures.get(fingerprint) ?? 0;
328
+ navigationState.unmatchedProvisionalFailures.set(fingerprint, unmatchedCount + 1);
329
+ applyNavigationFailure(validatedUrl, isMainFrame);
330
+ };
331
+ const navigationFailureListener = (failureEvent, errorCode, _errorDescription, validatedUrl, isMainFrame, frameProcessId, frameRoutingId) => {
332
+ if (navigationState.lastLoadFailureEvent === failureEvent) {
333
+ return;
334
+ }
335
+ navigationState.lastLoadFailureEvent = failureEvent;
336
+ const fingerprint = navigationFailureFingerprint(errorCode, validatedUrl, isMainFrame, frameProcessId, frameRoutingId);
337
+ const unmatchedCount = navigationState.unmatchedProvisionalFailures.get(fingerprint) ?? 0;
338
+ if (unmatchedCount > 0) {
339
+ // Electron exposes no navigation ID that can distinguish two same-target failures.
340
+ // Consume the possible provisional/load pair without disarming the current navigation;
341
+ // the renderer remains usable and is retired only if a later document actually commits.
342
+ if (unmatchedCount === 1) {
343
+ navigationState.unmatchedProvisionalFailures.delete(fingerprint);
344
+ } else {
345
+ navigationState.unmatchedProvisionalFailures.set(fingerprint, unmatchedCount - 1);
346
+ }
347
+ return;
348
+ }
349
+ applyNavigationFailure(validatedUrl, isMainFrame);
217
350
  };
218
351
  const renderProcessGoneListener = () => {
219
352
  this.retireRenderer(rendererLeaseId, renderer, true, 'render-process-gone');
220
353
  };
221
354
  renderer.destroyedListener = destroyedListener;
355
+ renderer.navigationStartListener = navigationStartListener;
356
+ renderer.navigationRedirectListener = navigationRedirectListener;
222
357
  renderer.navigationListener = navigationListener;
358
+ renderer.navigationFailureListener = navigationFailureListener;
359
+ renderer.navigationProvisionalFailureListener = navigationProvisionalFailureListener;
223
360
  renderer.renderProcessGoneListener = renderProcessGoneListener;
224
361
  event.sender.once('destroyed', destroyedListener);
362
+ event.sender.on('did-start-navigation', navigationStartListener);
363
+ event.sender.on('did-redirect-navigation', navigationRedirectListener);
225
364
  event.sender.on('did-navigate', navigationListener);
365
+ event.sender.on('did-fail-load', navigationFailureListener);
366
+ event.sender.on('did-fail-provisional-load', navigationProvisionalFailureListener);
226
367
  event.sender.on('render-process-gone', renderProcessGoneListener);
227
368
  try {
228
369
  this.assertActiveLifecycle();
@@ -239,6 +380,24 @@ class ElectronMainBleBinding {
239
380
  }
240
381
  return response;
241
382
  }
383
+ navigationState(sender) {
384
+ const existing = this.navigationStates.get(sender);
385
+ if (existing !== undefined) {
386
+ return existing;
387
+ }
388
+ const created = {
389
+ epoch: 0,
390
+ pendingReplacement: false,
391
+ lastStartDetails: null,
392
+ supersededTargetUrls: [],
393
+ unmatchedProvisionalFailures: new Map(),
394
+ lastProvisionalFailureEvent: null,
395
+ lastLoadFailureEvent: null,
396
+ sourceFrame: null
397
+ };
398
+ this.navigationStates.set(sender, created);
399
+ return created;
400
+ }
242
401
  assertMainFrame(event) {
243
402
  if (event === null || event === undefined || event.sender === null || event.sender === undefined) {
244
403
  throw (0, _errors.contractError)('protocol.malformed', 'ipc', 'electron-main-binding.frame-identity');
@@ -440,12 +599,14 @@ class ElectronMainBleBinding {
440
599
  if (cleanup.state === 'release-failed') {
441
600
  this.scheduleRendererReleaseRetry(rendererLeaseId, renderer);
442
601
  }
602
+ return cleanup;
443
603
  } catch (error) {
444
604
  console.error('[ElectronMainBleBinding] Authoritative renderer release rejected:', {
445
605
  rendererLeaseId,
446
606
  error
447
607
  });
448
608
  this.scheduleRendererReleaseRetry(rendererLeaseId, renderer);
609
+ return null;
449
610
  }
450
611
  }
451
612
  scheduleRendererReleaseRetry(rendererLeaseId, renderer) {
@@ -522,6 +683,22 @@ class ElectronMainBleBinding {
522
683
  renderer.sender.removeListener('did-navigate', renderer.navigationListener);
523
684
  renderer.navigationListener = null;
524
685
  }
686
+ if (renderer.navigationStartListener !== null) {
687
+ renderer.sender.removeListener('did-start-navigation', renderer.navigationStartListener);
688
+ renderer.navigationStartListener = null;
689
+ }
690
+ if (renderer.navigationRedirectListener !== null) {
691
+ renderer.sender.removeListener('did-redirect-navigation', renderer.navigationRedirectListener);
692
+ renderer.navigationRedirectListener = null;
693
+ }
694
+ if (renderer.navigationFailureListener !== null) {
695
+ renderer.sender.removeListener('did-fail-load', renderer.navigationFailureListener);
696
+ renderer.navigationFailureListener = null;
697
+ }
698
+ if (renderer.navigationProvisionalFailureListener !== null) {
699
+ renderer.sender.removeListener('did-fail-provisional-load', renderer.navigationProvisionalFailureListener);
700
+ renderer.navigationProvisionalFailureListener = null;
701
+ }
525
702
  if (renderer.renderProcessGoneListener !== null) {
526
703
  renderer.sender.removeListener('render-process-gone', renderer.renderProcessGoneListener);
527
704
  renderer.renderProcessGoneListener = null;
@@ -529,6 +706,9 @@ class ElectronMainBleBinding {
529
706
  }
530
707
  }
531
708
  exports.ElectronMainBleBinding = ElectronMainBleBinding;
709
+ function navigationFailureFingerprint(errorCode, validatedUrl, isMainFrame, frameProcessId, frameRoutingId) {
710
+ return JSON.stringify([errorCode, validatedUrl, isMainFrame, frameProcessId, frameRoutingId]);
711
+ }
532
712
  function createBoundRenderer(event, trusted, rendererLease) {
533
713
  return {
534
714
  rendererLease,
@@ -551,7 +731,11 @@ function createBoundRenderer(event, trusted, rendererLease) {
551
731
  retryHandle: null,
552
732
  releaseResult: null,
553
733
  destroyedListener: null,
734
+ navigationStartListener: null,
735
+ navigationRedirectListener: null,
554
736
  navigationListener: null,
737
+ navigationFailureListener: null,
738
+ navigationProvisionalFailureListener: null,
555
739
  renderProcessGoneListener: null
556
740
  };
557
741
  }
@@ -579,4 +763,38 @@ function assertEventLease(renderer, event) {
579
763
  throw (0, _errors.contractError)('ownership.denied', 'ipc', 'electron-main-binding.event-lease');
580
764
  }
581
765
  }
766
+
767
+ /** Narrows the untrusted IPC wire value before any handler dereferences request fields. */
768
+ function assertElectronBleIpcRequest(request) {
769
+ const record = recordValue(request);
770
+ if (record === null) {
771
+ throw (0, _errors.contractError)('protocol.malformed', 'ipc', 'electron-main-binding.request');
772
+ }
773
+ if (record.kind === 'bootstrap') {
774
+ return;
775
+ }
776
+ if (record.kind === 'route' && recordValue(record.envelope) !== null) {
777
+ return;
778
+ }
779
+ if (record.kind === 'release' && recordValue(record.rendererLease) !== null) {
780
+ return;
781
+ }
782
+ if (record.kind === 'event.ack' && recordValue(record.rendererLease) !== null && typeof record.eventId === 'string') {
783
+ return;
784
+ }
785
+ throw (0, _errors.contractError)('protocol.malformed', 'ipc', 'electron-main-binding.request');
786
+ }
787
+ function electronIpcRequestKind(request) {
788
+ const record = recordValue(request);
789
+ return typeof record?.kind === 'string' ? record.kind : 'malformed';
790
+ }
791
+ function recordValue(value) {
792
+ return isRecord(value) ? value : null;
793
+ }
794
+ function isRecord(value) {
795
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
796
+ }
797
+ function isRollbackReleaseRequiredError(error) {
798
+ return error instanceof _errors.BackendContractError && error.normalized.code === 'lifecycle.invalid-state' && error.normalized.operation === 'electron-main-router.rollback-release-required';
799
+ }
582
800
  //# sourceMappingURL=main-binding.js.map