Événements
Quarante événements de cycle de vie, de la demande de caméra à l’arrivée du résultat — tout ce que le widget sait de lui-même
Le widget rapporte ce qu’il fait par options.onEvent. Il y a quarante événements, couvrant tout le parcours : l’instance, les écrans, le consentement, le détecteur de visage, la caméra, le placement, les jetons, la mesure, le transport, et les panneaux superposés au parcours.
const instance = SaphereScan.create("#scan", {
onEvent: event => console.log(event.type, event),
proxy: { retrieveAccessToken: { strategy: "delegate", url: "/api/saphere-token" } }
})
Deux canaux, une différence
onEvent et les hooks se ressemblent et ne sont pas la même chose. La différence est le contrat, non le vocabulaire.
onEvent | hooks | |
|---|---|---|
| Objet | Observer | Agir |
| Valeur de retour | Ignorée | Un false littéral refuse |
| Attendu | Non | Oui, dans la limite d’un délai de garde de 2 secondes |
| Une exception ou un rejet | Rapporté en console, le parcours continue | Rapporté en console, traité comme une autorisation |
Le widget n’attend pas onEvent et ne lit pas ce qu’il retourne. C’est ce qui garantit qu’un gestionnaire lent ou fautif ne peut pas emporter une mesure avec lui. Si vous avez besoin de refuser quelque chose plutôt que de l’observer, c’est à cela que servent les hooks.
Lire un événement
Les noms sont de la forme domaine:événement. Chaque événement porte à plat ce qui le décrit — votre gestionnaire lit event.percent, non event.data.percent.
En TypeScript l’union est exportée, de sorte qu’un switch sur event.type est exhaustif et que chaque branche affine le type :
import type { SaphereScanEvent } from "…"
const onEvent = (event: SaphereScanEvent) => {
switch (event.type) {
case "measure:result": return save(event.result)
case "measure:failed": return report(event.code)
case "camera:error": return explain(event.code)
}
}
En JavaScript, utilisez SAPHERE_SCAN_EVENTS pour disposer des noms sous forme de constantes — une faute de frappe dans un littéral de chaîne nu est une branche qui ne s’exécute jamais en silence.
widget — l’instance
| Événement | Charge utile | Quand |
|---|---|---|
widget:ready | — | Le widget est monté. Toujours le premier événement. |
widget:error | message | bootstrap() a échoué. La promesse qu’il retourne est rejetée avec la même erreur. |
widget:destroyed | — | destroy() a été appelé. Rien ne suit. |
widget:error existe parce que la promesse rejetée est facile à manquer : une intégration qui appelle bootstrap() sans l’attendre n’aurait autrement aucun signe de l’échec du montage.
screen — le parcours
| Événement | Charge utile | Quand |
|---|---|---|
screen:enter | path, name | Un écran est affiché, y compris le premier. |
screen:blocked | from, to | Votre hook beforeScreenChange a refusé la transition. |
path est la route (/onboarding/1, /measurement, /result) ; name est l’écran (intro, onboarding, load-face-detectors, measurement, result, error).
Suivez `name`, non `path`
L’indice d’onboarding dépend des écrans que vous avez désactivés. Aveconboarding3: { ignore: true }, /onboarding/2 est le quatrième écran chez vous et le troisième chez quelqu’un d’autre. name est stable d’une configuration à l’autre.consent et form — ce que l’utilisateur accorde
| Événement | Charge utile | Quand |
|---|---|---|
consent:changed | termsOfUse, privacy | L’une des deux cases à cocher a changé. |
consent:accepted | — | L’écran a été quitté avec les deux cases cochées. |
form:completed | — | Le formulaire d’informations personnelles a été quitté, valide. |
consent:accepted est émis en quittant l’écran dans les deux sens, non au moment où la seconde case est cochée : tant que l’écran est là, l’utilisateur peut encore décocher.
form:completed ne porte rien. Ce sont des données de santé, et elles vous parviennent déjà dans le userData du résultat — elles n’ont aucune raison de voyager deux fois.
detector — le détecteur de visage
| Événement | Quand |
|---|---|
detector:loading | Le chargement commence, au montage. |
detector:loaded | Le détecteur est prêt. |
detector:error | Le détecteur n’a pas pu être chargé : aucune mesure ne sera possible. |
detector:error mérite d’être branché. Il laisse l’écran de chargement sans issue — pas de motif d’échec, pas de bouton, rien sur quoi l’utilisateur puisse agir — c’est donc le seul cas où votre propre traitement des erreurs doit prendre le relais.
camera — la caméra
| Événement | Charge utile | Quand |
|---|---|---|
camera:requesting | — | L’autorisation est demandée. Une fois par tentative d’ouverture. |
camera:ready | width, height, frameRate | Le flux est ouvert. |
camera:error | code, name | Le flux a été refusé. |
camera:ended | — | La caméra s’est arrêtée d’elle-même : débranchée, prise par une autre application. |
camera:released | — | Le widget a rendu la caméra. |
camera:devices-changed | — | Un périphérique a été branché ou débranché ; le flux se recharge. |
code vaut NOT_ALLOWED, NOT_FOUND, NOT_READABLE, ABORT, NOT_SUPPORTED ou UNKNOWN. name est le nom d’exception brut produit par le navigateur, les navigateurs ne les employant pas tous de la même façon — conservez-le pour le diagnostic, branchez sur code.
`camera:ready` rapporte ce qui a été accordé, non ce qui a été demandé
Le widget demande 30 images par seconde en 640×480 et se rabat silencieusement sur ce que la caméra veut bien donner. UnframeRate de 15 explique une mesure de moindre qualité, et cet événement est le seul moyen de le voir. Les identifiants de matériel ne sont jamais transmis.camera:ended et camera:released sont deux états distincts. Le premier n’est pas voulu — la source s’est arrêtée d’elle-même, et la mesure est compromise. Le second est le widget qui fait le ménage.
placement — le cadrage du visage
| Événement | Charge utile | Quand |
|---|---|---|
placement:started | — | L’écran de placement est actif. Peut être émis deux fois si la caméra change. |
placement:guidance | guidance, stable | La consigne affichée a changé. |
placement:accepted | — | Le visage est accepté et le cadrage figé. |
guidance vaut "up", "down", "back" ou null — null signifiant soit que rien n’est à corriger, soit qu’aucun visage n’est vu.
L’événement n’est émis qu’au changement. La détection tourne une soixantaine de fois par seconde ; rapporter chaque image noierait un gestionnaire sans bénéfice.
token — le jeton d’accès
| Événement | Charge utile | Quand |
|---|---|---|
token:requesting | strategy | Un jeton est demandé, à chaque tentative. |
token:retrieved | — | Un jeton exploitable a été obtenu. |
token:error | code | Il n’a pas pu l’être. |
code sépare votre échec du nôtre — voyez les jetons d’accès pour le tableau. C’est le seul point du parcours où la faute peut être de votre côté, ce qui est la raison pour laquelle un code générique unique ne suffisait pas.
La valeur du jeton n’est jamais portée par un événement.
measure — la mesure
| Événement | Charge utile | Quand |
|---|---|---|
measure:attempt | attempt | Une tentative commence. Numérotée à partir de un. |
measure:start | — | L’acquisition démarre : les trente secondes de capture courent. |
measure:progress | percent | La progression a changé d’un point entier. |
measure:captures-sent | totalCaptures, totalImages | Tout a été envoyé ; le serveur calcule. |
measure:result | result | Le résultat est arrivé et a été validé. |
measure:aborted | code | Arrêt délibéré : l’utilisateur a renoncé, ou votre hook a refusé. |
measure:failed | code | Panne : la mesure n’aboutira pas. |
La séparation entre aborted et failed existe pour votre supervision. Une annulation par l’utilisateur est un déroulement normal et ne doit pas apparaître dans vos tableaux de bord comme un incident.
code est un motif d’échec. REFUSED_BY_INTEGRATOR est celui que produit un veto de beforeMeasureStart, et il est classé parmi les arrêts — rien n’est cassé, quelqu’un a dit non.
measure:result est émis une fois la charge utile reçue et validée. Entre l’arrivée du résultat sur le fil et cet événement il y a une étape de validation, et une charge utile qui la manque produit measure:failed à la place — de sorte que vous ne recevez jamais un résultat que le widget lui-même n’afficherait pas.
transport — l’acheminement vers le serveur
| Événement | Charge utile | Quand |
|---|---|---|
transport:connected | — | La connexion est établie. |
transport:reconnecting | attempt | Une reconnexion est tentée (jusqu’à trente). |
transport:disconnected | reason | La connexion est perdue et ne revient pas d’elle-même. |
transport:degraded | — | Le débit ne suit pas ; la mesure continue. |
transport:restored | — | Le débit est revenu. |
transport:disconnected n’est émis qu’une fois que le client a cessé de réessayer. Pendant la reconnexion vous recevez transport:reconnecting à la place — rapporter une reprise automatique comme une déconnexion ferait passer chaque bref à-coup pour une panne.
ui — les couches superposées au parcours
| Événement | Charge utile |
|---|---|
ui:menu-opened, ui:menu-closed | — |
ui:document-opened, ui:document-closed | kind : notice, termsOfUse ou privacy |
ui:language-changed | locale, direction (ltr ou rtl) |
ui:language-changed porte la direction parce que c’est ce sur quoi vous pouvez avoir à agir : une mise en page hôte encadrant le widget peut avoir à se retourner elle aussi.
Garanties d’ordonnancement
Deux, et elles sont structurelles plutôt que fortuites.
widget:ready est toujours le premier événement. Le montage de l’application déclenche un rendu synchrone, de sorte que les services qui émettent pendant le montage — le détecteur de visage, notamment — rapporteraient sinon avant que le widget ne se soit annoncé. Ces événements sont retenus et délivrés derrière widget:ready.
Rien ne suit widget:destroyed. Le démontage de l’application exécute tous les crochets de destruction, dont plusieurs émettent. Le canal se verrouille avant que cela n’arrive, de sorte que l’événement est véritablement le dernier.
Au-delà de ces deux garanties, un échec émet son événement de mesure puis le changement d’écran — measure:failed, puis screen:enter pour /error.
Ce qui n’est délibérément pas émis
Trois absences, chacune pour une raison qui mérite d’être connue avant d’aller les chercher.
Aucun événement par message ré-émis. Une mesure met quelque neuf cents captures en vol, chaque accusé de réception expirant au bout de cinquante secondes. Une coupure produirait une rafale de neuf cents événements sur un canal qui, dans une intégration mobile, se trouve derrière un pont WebView. transport:degraded couvre le cas en deux événements à la place.
Aucun événement de redimensionnement. La taille du conteneur est votre mise en page, que vous connaissez déjà, et ResizeObserver se déclenche à chaque image d’animation pendant un glissement.
Aucun événement « calcul en cours » distinct. La fin de l’envoi et le début du calcul se produisent dans le même tour synchrone. measure:captures-sent le dit une fois, et porte les totaux.
Migrer
Depuis le widget v1
| v1 | v2 |
|---|---|
transition (from → to) | screen:enter — déclaré en v1 mais jamais réellement émis |
video-stream-loading status: "start" | camera:requesting |
video-stream-loading status: "ready" | camera:ready, avec les réglages obtenus |
video-stream-loading status: "error", reason | camera:error, code |
start | placement:accepted |
record | measure:start |
end | measure:captures-sent, avec les totaux |
result | measure:result |
aborted | measure:aborted ou measure:failed, selon que l’arrêt était délibéré |
leave | — la v2 n’a pas de commande pour quitter le widget |
onEvent retournant false annulait la transition | hooks.beforeScreenChange |
Correspondance des motifs caméra : denied → NOT_ALLOWED, no-device → NOT_FOUND, already-used → NOT_READABLE, not-supported → NOT_SUPPORTED, plus ABORT, qui est nouveau.
Le contrat a changé sur un point : onEvent n’est plus attendu et son retour n’est plus lu. C’est ce qui garantit qu’un gestionnaire lent ou fautif ne peut pas emporter une mesure. La faculté de refuser une transition n’a pas disparu — elle est passée dans les hooks, où elle est bornée par un délai de garde.
Depuis les premières intégrations v2
Les six premiers noms ont été remplacés par les noms à espace de nommage :
| Avant | Maintenant |
|---|---|
ready | widget:ready |
screen | screen:enter — porte aussi name |
progress | measure:progress |
result | measure:result |
abort | measure:aborted |
error | measure:failed |