É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.

onEventhooks
ObjetObserverAgir
Valeur de retourIgnoréeUn false littéral refuse
AttenduNonOui, dans la limite d’un délai de garde de 2 secondes
Une exception ou un rejetRapporté en console, le parcours continueRapporté 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énementCharge utileQuand
widget:readyLe widget est monté. Toujours le premier événement.
widget:errormessagebootstrap() a échoué. La promesse qu’il retourne est rejetée avec la même erreur.
widget:destroyeddestroy() 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énementCharge utileQuand
screen:enterpath, nameUn écran est affiché, y compris le premier.
screen:blockedfrom, toVotre 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. Avec onboarding3: { 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.
ÉvénementCharge utileQuand
consent:changedtermsOfUse, privacyL’une des deux cases à cocher a changé.
consent:acceptedL’écran a été quitté avec les deux cases cochées.
form:completedLe 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énementQuand
detector:loadingLe chargement commence, au montage.
detector:loadedLe détecteur est prêt.
detector:errorLe 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énementCharge utileQuand
camera:requestingL’autorisation est demandée. Une fois par tentative d’ouverture.
camera:readywidth, height, frameRateLe flux est ouvert.
camera:errorcode, nameLe flux a été refusé.
camera:endedLa caméra s’est arrêtée d’elle-même : débranchée, prise par une autre application.
camera:releasedLe widget a rendu la caméra.
camera:devices-changedUn 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. Un frameRate 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énementCharge utileQuand
placement:startedL’écran de placement est actif. Peut être émis deux fois si la caméra change.
placement:guidanceguidance, stableLa consigne affichée a changé.
placement:acceptedLe visage est accepté et le cadrage figé.

guidance vaut "up", "down", "back" ou nullnull 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énementCharge utileQuand
token:requestingstrategyUn jeton est demandé, à chaque tentative.
token:retrievedUn jeton exploitable a été obtenu.
token:errorcodeIl 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énementCharge utileQuand
measure:attemptattemptUne tentative commence. Numérotée à partir de un.
measure:startL’acquisition démarre : les trente secondes de capture courent.
measure:progresspercentLa progression a changé d’un point entier.
measure:captures-senttotalCaptures, totalImagesTout a été envoyé ; le serveur calcule.
measure:resultresultLe résultat est arrivé et a été validé.
measure:abortedcodeArrêt délibéré : l’utilisateur a renoncé, ou votre hook a refusé.
measure:failedcodePanne : 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énementCharge utileQuand
transport:connectedLa connexion est établie.
transport:reconnectingattemptUne reconnexion est tentée (jusqu’à trente).
transport:disconnectedreasonLa connexion est perdue et ne revient pas d’elle-même.
transport:degradedLe débit ne suit pas ; la mesure continue.
transport:restoredLe 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énementCharge utile
ui:menu-opened, ui:menu-closed
ui:document-opened, ui:document-closedkind : notice, termsOfUse ou privacy
ui:language-changedlocale, 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

v1v2
transition (fromto)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", reasoncamera:error, code
startplacement:accepted
recordmeasure:start
endmeasure:captures-sent, avec les totaux
resultmeasure:result
abortedmeasure: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 transitionhooks.beforeScreenChange

Correspondance des motifs caméra : deniedNOT_ALLOWED, no-deviceNOT_FOUND, already-usedNOT_READABLE, not-supportedNOT_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 :

AvantMaintenant
readywidget:ready
screenscreen:enter — porte aussi name
progressmeasure:progress
resultmeasure:result
abortmeasure:aborted
errormeasure:failed