CurrentValueSubject, pas Passthrough : il fallait réveiller AVANT, pas après

La lecture des sources tranche ce que trois essais n'avaient pas résolu, et
elle retourne mon raisonnement précédent.

`bleStateSubject` est un `CurrentValueSubject<BleState, Never>(.unknown)` : il
rejoue sa valeur à chaque abonnement. Être abonné au moment de l'émission
n'apporte donc rien — et le correctif intermédiaire, qui réveillait le manager
APRÈS s'être abonné « pour ne pas rater l'événement », était un raisonnement de
PassthroughSubject appliqué au mauvais type.

Pire, il exposait à un second piège : `centralManagerDidUpdateState` fait
`BleState(rawValue: self.manager.state.rawValue)`, soit un accès à la lazy var
DEPUIS le délégué. Réveillée au milieu d'un abonnement, la propriété peut se
réentrer avant la fin de sa propre initialisation.

Séquence corrigée : créer le listener, poser le filtre, sonder `blePowered()`
jusqu'à ce qu'il réponde vrai — hors de tout abonnement, le premier appel
instancie, les suivants observent — puis appeler search() une seule fois. Si
l'état n'est pas atteint en 10 s, on le dit au lieu de scanner dans le vide.

Le commentaire qui affirmait « bleStateSubject ne publie que sur changement » est
corrigé plutôt que laissé : il était faux, et un commentaire qui ment coûte plus
cher que pas de commentaire. L'abonnement unique reste, mais pour la vraie
raison — chaque abonnement relance le cycle addClient/removeClient du scanner, et
la découverte BLE demande plusieurs secondes ininterrompues.

82 tests au vert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-08-31 13:27:58 +00:00
parent 0f96122cca
commit 753d7b248e
2 changed files with 80 additions and 37 deletions

View File

@@ -166,13 +166,28 @@ abonné à `search()`. L'ordre est critique : si `bleStateSubject` est un
`PassthroughSubject`, un état émis avant l'abonnement serait perdu et on
retomberait sur le même silence.
## 🔴 ÉTAT AU SOIR DU 31/08 : bloqué, et pourquoi il faut changer d'approche
## Le démarrage du SDK — deux détails qui décident de tout
Malgré le réveil du manager, le filtre Polar et l'abonnement unique, le
publisher de `search()` **reste muet** : `bleStateSubject` n'émet jamais
`.poweredOn`. Le manager du SDK existe pourtant — l'avertissement CoreBluetooth
`SDKCBCentralManager … has no restore identifier` ne peut apparaître que s'il a
été instancié.
Lus dans les sources, après plusieurs essais infructueux le 31/08 :
**1. `bleStateSubject` est un `CurrentValueSubject<BleState, Never>(.unknown)`**,
pas un PassthroughSubject. Il **rejoue sa valeur à chaque abonnement** : il est
donc inutile d'être abonné au moment de l'émission. Un correctif intermédiaire
réveillait le manager *après* s'être abonné « pour ne pas rater l'événement » —
raisonnement juste pour un PassthroughSubject, faux ici, et qui exposait au
piège suivant.
**2. `centralManagerDidUpdateState` fait
`BleState(rawValue: self.manager.state.rawValue)`** — il accède à la `lazy var`
depuis le délégué. Si CoreBluetooth appelle le délégué avant que l'initialisation
de la lazy soit terminée, la propriété se réentre.
**Séquence correcte** : créer le listener, poser `scanPreFilter`, réveiller la
lazy en sondant `blePowered()` jusqu'à ce qu'elle réponde vrai — hors de tout
abonnement — et seulement ensuite appeler `search()`, une fois, sur toute la
durée. Un abonnement par tranche relancerait le cycle
`addClient()`/`removeClient()` du scanner et l'empêcherait de découvrir quoi que
ce soit.
**Ce qui a été corrigé et vérifié en chemin** (ne pas refaire) :