Les notifications push web et leurs abonnements jetables
Un abonnement Web Push n'est pas un compte, le navigateur le délivre et en dispose. À Ramedex, le téléphone renvoie le sien au serveur à chaque ouverture, et l'ancien est écrasé. Avec une surprise côté serveur.
Ramedex fait capturer les vrais trains qui passent à côté du joueur. Les notifications push y servent à prévenir qu'un train rare approche. Ce genre de message a toutes les chances d'arriver bien après la première visite, un matin où le joueur n'a pas ouvert le jeu depuis longtemps. Entre les deux, son abonnement push a eu le temps de disparaître ou de changer de clés, et rien de tout ça ne se voit depuis le serveur.
L'abonnement appartient au navigateur
Un abonnement Web Push n'est pas un compte, c'est un endpoint, une URL propre à l'appareil chez le service de push du navigateur, avec deux clés pour chiffrer la charge. C'est le navigateur qui le délivre et qui en dispose. Il peut le faire expirer, en régénérer un, ou le laisser mourir quand le joueur vide les données du site. L'idée qui vient en premier est de l'enregistrer au moment où le joueur accepte les notifications, puis de le considérer comme acquis. Elle tient le jour de la mise en ligne, puis se dégrade tranquillement.
Le renvoyer à chaque ouverture
Le parti pris de Ramedex est de le traiter comme jetable. À chaque ouverture, le téléphone renvoie son abonnement au serveur. S'il n'en a plus, il s'abonne. Si les clés ont changé, il repart d'un abonnement neuf et l'ancien est écrasé. L'abonnement stocké côté serveur a donc au plus l'âge de la dernière ouverture.
Voici le principe côté navigateur, en version simplifiée.
const registration = await navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidKey, // clé publique du serveur, en Uint8Array
});
}
await fetch("/push/subscription", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(subscription.toJSON()),
});Il n'y a rien de conditionnel là-dedans. L'appel part à chaque ouverture, même si l'abonnement n'a pas changé depuis la veille. Ce choix colle à l'usage du jeu. Une alerte de train rare n'a de valeur que sur un téléphone qui sert encore. Or un appareil qui sert encore finit par rouvrir Ramedex, donc par renvoyer son abonnement.
La surprise était côté serveur
Sans les extensions PHP gmp et bcmath dans l'image Docker, l'envoi échouait. La bibliothèque qui expédie les notifications en dépend, et l'image officielle de PHP ne les embarque pas. Une fois les deux extensions ajoutées, les alertes sont passées.
Ce que j'en retiens est un rapport de coûts. Réabonner à chaque ouverture tient en quelques lignes côté navigateur. Gérer des abonnements morts demande d'écouter les erreurs renvoyées par les endpoints, de nettoyer, de réessayer, et d'accepter qu'une partie des envois parte dans le vide pendant ce temps. Je préfère de loin la première option. Il me reste les téléphones qui n'ouvrent plus jamais le jeu. Leur abonnement périmé traîne côté serveur, et rien ne le signale avant le premier envoi raté.