Steven
Steven7 min de lecture

Une connexion perdue ne devrait pas faire planter toute ton app — mais la nôtre le faisait

Quand notre backend est tombé en pleine réunion, il n'a pas seulement mis la transcription en pause — il a fait planter l'app entière. La cause était un unique événement non géré, et le correctif faisait dix lignes. Voici la grappe de versions qui a fait que GeekBye reste connecté à ton compte et reste connecté au réseau à travers les choses qui le tuaient avant.

Ingénierie
Fiabilité
Electron
Sorties GeekBye
Une connexion perdue ne devrait pas faire planter toute ton app — mais la nôtre le faisait

Certains bugs interrompent une fonctionnalité. Les pires font tomber tout ce qui les entoure. Dans une build précoce de GeekBye, si notre backend tombait pendant que tu étais en pleine réunion, la transcription ne s''arrêtait pas seulement — l''app entière plantait. Une connexion perdue, et toute la fenêtre disparaissait.

La grappe de versions qui a corrigé cela — de v1.6.8 à v1.6.11 — est une masterclass sur le travail peu glamour de "reste connecté à ton compte, reste connecté au réseau". Le correctif vedette faisait dix lignes. Voici ce qu''étaient ces dix lignes, et tout le reste qui a été livré à leurs côtés.

Dix lignes entre une déconnexion et un plantage

Le plantage vivait dans le code audio. Quand une session de transcription démontait ses WebSockets, elle appelait removeAllListeners() dessus — un nettoyage raisonnable. Mais voici le détail de Node.js qui transforme le nettoyage en catastrophe : un événement error sans listener n''est pas ignoré. Il est relancé comme une exception non capturée.

Alors imagine la séquence quand le backend tombe : le socket a un événement error en file d''attente, prêt à se déclencher. Le nettoyage exécute removeAllListeners(), retirant le gestionnaire qui l''aurait capturé. Un instant plus tard l''erreur en attente se déclenche — dans le vide. Node voit un événement error sans personne à l''écoute, et fait la seule chose que son contrat autorise : il le lance comme une exception non capturée, qui fait planter le processus. Le backend de l''utilisateur a hoqueté deux secondes et son app s''est évaporée.

Le correctif (v1.6.8) est presque anticlimatique. Juste après avoir retiré les listeners, réattacher un gestionnaire d''erreur no-op délibéré :

this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events

Cette fonction vide est tout l''enjeu. Elle donne à l''erreur en attente un endroit où atterrir — un puits — pour que Node ait un listener et ne relance jamais. Le même commit a aussi nettoyé les sockets à moitié ouverts quand une tentative de connexion échoue à mi-chemin, pour qu''une connexion bâclée ne puisse pas laisser un socket vivant traîner avec une erreur en attente de se déclencher. Le commentaire du commit le dit sans détour : "Sans ceci, les erreurs en attente deviennent des exceptions non capturées et font planter l''app." Dix lignes ont transformé un plantage complet en une déconnexion silencieuse et récupérable.

La leçon vaut plus que le correctif : chaque fois que tu appelles removeAllListeners() sur un socket ou un stream que tu es peut-être encore en train de démonter, réattache d''abord un puits d''erreur error no-op. Un événement error non géré est un plantage, pas une ligne de log.

Rester connecté à ton compte : rafraîchir en avance, et rafraîchir au rebond

La moitié "reste connecté au réseau" avait une jumelle "reste connecté à ton compte". Avant cette grappe, le token d''auth de GeekBye expirait tout simplement — au bout d''un moment tu étais renvoyé sans cérémonie vers l''écran de connexion, en plein flux de travail, sans aucune raison visible.

v1.6.8 a corrigé cela dans les deux directions :

  • Proactif : un minuteur rafraîchit le token avant qu''il n''expire, programmé à l''expiration moins une marge intelligente (une fraction de la durée de vie du token, plancher et plafond appliqués). Tu es rafraîchi avant même de le remarquer.
  • Réactif : si une requête revient avec 401 malgré tout — décalage d''horloge, un minuteur raté, un portable froid — le client rafraîchit une fois et réessaie la requête originale exactement une fois. Un unique flag de garde plafonne ce retry pour qu''un 401 persistant ne puisse jamais tourner en boucle infinie.

La partie subtile mais critique : les deux chemins sont derrière un mutex. Si dix requêtes reçoivent un 401 au même instant, elles ne déclenchent pas dix rafraîchissements — elles attendent toutes l''unique rafraîchissement en cours puis réessaient. Et quand un rafraîchissement ne peut réellement pas être sauvé, l''app fait expirer la session avec grâce — une notification claire de dix secondes "ta session a expiré, merci de te reconnecter" — au lieu d''une expulsion silencieuse vers l''écran de connexion.

Reconnexion : ajoute-la côté client, puis déplace-la vers le backend

v1.6.9 a rendu une connexion de transcription perdue récupérable au lieu de fatale. Quand le backend signalait que la connexion vocale en amont avait chuté, le client cessait d''afficher une erreur fatale et se reconnectait à la place — avec une UX délibérément calme : une unique notification ambre "Reconnexion…" (pas une par tentative), une notification verte "Reconnecté" quand elle revenait, et l''erreur terminale "merci de redémarrer" seulement après que chaque tentative fut épuisée.

Puis quelque chose que j''aime dans cette grappe : une version plus tard, dans v1.6.10, nous avons supprimé cette logique de reconnexion côté client — environ 113 lignes — et déplacé la reconnexion vers le backend, qui émet désormais de discrets messages de statut "reconnexion" que le client se contente de refléter. Nous avons construit la récupération côté client, décidé que le backend en était le bon propriétaire, et l''avons déplacée. Ce n''est pas de l''hésitation ; c''est le cycle de vie honnête d''un problème difficile. Une logique de fiabilité éclatée sur les deux extrémités d''une connexion est une mauvaise odeur — choisis un propriétaire. (Le backend a fini par devenir le propriétaire définitif de cela, ce qui est l''histoire de reconnexion racontée dans pourquoi ton preneur de notes IA s''arrête sur un mauvais Wi-Fi.)

La même version a rendu les limites de temps d''enregistrement humaines : au lieu d''une erreur brute, une notification d''avertissement qui laisse l''enregistrement continuer, et un message clair "Limite d''Enregistrement Atteinte" quand il s''arrête vraiment — la phrase amicale affichée, le préfixe interne retiré.

Un seul retry pour les gouverner tous

À v1.6.11, la même logique de retry-avec-backoff avait été copiée-collée à trois endroits différents — auth, mises à jour, démontage de session. Elle a donc été consolidée en un unique module avec une seule règle partagée sur ce qui vaut la peine d''être réessayé : les échecs réseau transitoires (connexion réinitialisée, timeout, hoquet DNS, socket qui raccroche) sont réessayés avec backoff exponentiel ; une vraie erreur serveur échoue vite, parce que réessayer un vrai 4xx ne fait que gaspiller le temps de l''utilisateur. Un classifieur, quelques presets nommés, trois appelants migrés dessus.

Ce que cette grappe a enseigné

  1. Un événement error non géré est un plantage, pas une ligne de log. Node relance les événements error qui n''ont pas de listener. Retire les listeners d''un socket que tu es peut-être encore en train de démonter, et tu dois réattacher un puits d''erreur no-op — sinon une connexion perdue emporte l''app entière avec elle.
  2. Rafraîchis les identifiants avant l''expiration et réactivement sur un 401 — avec un mutex sur les deux. Le proactif te garde connecté ; le réactif attrape les cas limites ; le mutex signifie que N appelants simultanés causent exactement un rafraîchissement, et un plafond de retry signifie qu''un mauvais token ne peut pas boucler éternellement.
  3. Ne réessaie que sur les erreurs transitoires, et centralise le classifieur. "Est-ce que ça vaut la peine d''être réessayé ?" est une seule décision qui appartient à un seul endroit. Réessayer une vraie erreur n''est qu''un échec plus lent.
  4. Décide qui possède la reconnexion. Nous l''avons ajoutée côté client, puis déplacée vers le backend. Une logique de récupération vivant sur les deux extrémités est un générateur de bugs ; donne-lui un foyer unique.

Ceci est le troisième chapitre de l''histoire de fiabilité qui devient GeekBye v2. Pour le chapitre précédent, vois nous avons supprimé 5 000 lignes de code audio et les transcriptions ont commencé à apparaître en double (v1.6.0) ; pour l''endroit où la reconnexion s''est finalement posée, pourquoi ton preneur de notes IA s''arrête sur un mauvais Wi-Fi ; et pour tout l''arc, l''anatomie de livrer un logiciel jusqu''à la perfection.

Articles Connexes

La vérification de sécurité qui a rendu notre app impossible à quitter
Steven
Steven7 min de lecture

La vérification de sécurité qui a rendu notre app impossible à quitter

La mise à jour automatique a été la fonctionnalité la plus difficile que nous ayons jamais livrée — six versions en quatre jours pour l'empêcher de rendre l'app inutilisable. Le pire bug, c'est nous qui l'avons introduit en voulant être prudents : une vérification « de sécurité » de 500 millisecondes qui transformait une mise à jour ratée en un processus qu'on ne pouvait littéralement pas quitter.

Ingénierie
Electron
Fiabilité
Une variable CSS, cinq tours de revue et une chaîne d'outils Swift qui a menti
Steven
Steven8 min de lecture

Une variable CSS, cinq tours de revue et une chaîne d'outils Swift qui a menti

La v2.0.7 de GeekBye a rendu tout l'overlay translucide de façon ajustable — ce qui ressemble à une modification CSS d'une ligne et n'en était absolument pas une. La vraie histoire, c'est ce qu'a rattrapé la revue de code : deux surfaces qui refusaient de s'estomper, une barre d'enregistrement qui paraissait fausse à la même opacité, et un binaire compilé qui a oscillé de 672 octets parce que notre propre documentation a indiqué à un relecteur la mauvaise version de Swift.

Ingénierie
Design
Build
Le jour où notre app s'est DDoS elle-même
Steven
Steven6 min de lecture

Le jour où notre app s'est DDoS elle-même

Un backlog d'uploads en attente, relâché d'un seul coup au démarrage, a transformé chaque client GeekBye en une petite attaque par déni de service contre nos propres serveurs. Le correctif — et l'échelle de vérification de vitalité de la connexion qu'il nous a forcés à construire — est l'une des choses les plus utiles que la v2 nous ait apprises.

Fiabilité
Réseau
Ingénierie