blePowered() lit le manager, search() lit le sujet : ce ne sont pas les mêmes

Le message n'avait pas bougé alors qu'il n'aurait plus dû pouvoir apparaître :
attendre `blePowered()` avait donc réussi, et le publisher se taisait quand même.
C'est ce qui a montré l'erreur.

`blePowered()` rend `manager.state == .poweredOn` — l'état de CoreBluetooth. Or
`search()` filtre sur `bleStateSubject`, alimenté uniquement par
`centralManagerDidUpdateState`. Le manager peut donc être allumé sans que le
délégué ait publié quoi que ce soit : le sujet reste à `.unknown`, le filtre
bloque, le publisher se tait. Attendre `blePowered()` ne prouvait rien sur ce que
`search()` allait voir — c'était la mauvaise sonde.

On attend désormais sur `monitorBleState()`, qui est public et qui EST la source
lue par search(). `blePowered()` garde un seul rôle : instancier la lazy pour
que le délégué puisse tourner.

L'attente porte son échéance elle-même plutôt que de passer par avecEcheance :
le listener n'est pas Sendable et le faire traverser une TaskGroup se heurterait
à la concurrence stricte de Swift 6. Et `BoiteJeton` garantit une reprise unique
de la continuation — deux chemins de sortie concurrents (l'état publié et
l'échéance), et reprendre deux fois est un crash, pas un avertissement.

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:31:30 +00:00
parent 753d7b248e
commit eef76daa45
2 changed files with 79 additions and 13 deletions

View File

@@ -182,10 +182,18 @@ piège suivant.
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
**3. `blePowered()` et `search()` ne lisent pas la même chose.** `blePowered()`
rend `manager.state == .poweredOn` — l'état de CoreBluetooth. `search()` filtre
sur `bleStateSubject`, qui n'est alimenté que par `centralManagerDidUpdateState`.
Le manager peut donc être allumé sans que le délégué ait publié quoi que ce
soit : le sujet reste à `.unknown` et le filtre bloque. Attendre `blePowered()`
ne prouve rien sur ce que `search()` verra.
**Séquence correcte** : créer le listener, poser `scanPreFilter`, appeler
`blePowered()` **une fois** (son seul rôle : instancier la lazy pour que le
délégué puisse tourner), puis attendre sur **`monitorBleState()`** — public, et
c'est la source que `search()` lit — jusqu'à `.poweredOn`. Seulement ensuite,
`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.