
Una connessione caduta non dovrebbe mandare in crash tutta la tua app — ma la nostra lo faceva
Quando il nostro backend è andato offline nel mezzo di una riunione, non ha solo messo in pausa la trascrizione — ha mandato in crash l'intera app. La causa era un singolo evento non gestito, e la correzione erano dieci righe. Questo è il gruppo di release che ha reso GeekBye capace di restare connesso all'account e restare connesso alla rete attraverso le cose che prima lo uccidevano.
Alcuni bug interrompono una funzionalità. I peggiori abbattono tutto ciò che le sta intorno. In una build iniziale di GeekBye, se il nostro backend andava offline mentre eri nel mezzo di una riunione, la trascrizione non si fermava soltanto — l''intera app andava in crash. Una connessione caduta, e tutta la finestra spariva.
Il gruppo di release che ha risolto questo — da v1.6.8 a v1.6.11 — è una lezione magistrale sul lavoro poco glamour di "resta loggato, resta connesso". La correzione principale erano dieci righe. Ecco cosa erano quelle dieci righe, e tutto il resto che è stato rilasciato accanto ad esse.
Dieci righe tra una disconnessione e un crash
Il crash viveva nel codice audio. Quando una sessione di trascrizione smontava i suoi WebSocket, chiamava removeAllListeners() su di essi — una pulizia ragionevole. Ma ecco il dettaglio di Node.js che trasforma la pulizia in catastrofe: un evento error senza listener non viene ignorato. Viene rilanciato come eccezione non catturata.
Immagina quindi la sequenza quando il backend va offline: il socket ha un evento error in coda, pronto a scattare. La pulizia esegue removeAllListeners(), rimuovendo l''handler che lo avrebbe catturato. Un attimo dopo l''error in coda scatta — nel vuoto. Node vede un evento error con nessuno in ascolto, e fa l''unica cosa che il suo contratto permette: lo lancia come eccezione non catturata, che manda in crash il processo. Il backend dell''utente ha avuto un singhiozzo di due secondi e la sua app è svanita.
La correzione (v1.6.8) è quasi anticlimatica. Subito dopo aver rimosso i listener, riagganciare un handler di error no-op deliberato:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
Quella funzione vuota è tutto il punto. Dà all''error pendente un posto dove atterrare — un pozzo — così che Node abbia un listener e non rilanci mai. Lo stesso commit ha anche ripulito i socket semiaperti quando un tentativo di connessione fallisce a metà strada, così che una connessione mal riuscita non possa lasciare un socket vivo penzolante con un error in attesa di scattare. Il commento del commit lo dice chiaro: "Senza questo, gli errori pendenti diventano eccezioni non catturate e mandano in crash l''app." Dieci righe hanno trasformato un crash totale in una disconnessione silenziosa e recuperabile.
La lezione vale più della correzione: ogni volta che chiami removeAllListeners() su un socket o uno stream che potresti ancora star smontando, riaggancia prima un pozzo di error error no-op. Un evento error non gestito è un crash, non una riga di log.
Restare loggati: fai il refresh in anticipo, e fai il refresh al rimbalzo
La metà "resta connesso" aveva una gemella "resta loggato". Prima di questo gruppo, il token di auth di GeekBye semplicemente scadeva — dopo un po'' venivi scaricato senza cerimonie di nuovo alla schermata di login, nel mezzo del flusso di lavoro, senza nessuna ragione che potessi vedere.
v1.6.8 ha risolto questo da entrambe le direzioni:
- Proattivo: un timer fa il refresh del token prima che scada, programmato alla scadenza meno un margine intelligente (una frazione della vita del token, con pavimento e soffitto). Ti viene fatto il refresh prima ancora che tu te ne accorga.
- Reattivo: se una richiesta torna con
401comunque — sfasamento dell''orologio, un timer mancato, un laptop freddo — il client fa il refresh una volta e riprova la richiesta originale esattamente una volta. Un singolo flag di guardia limita quel retry così che un 401 persistente non possa mai girare in un loop infinito.
La parte sottile ma critica: entrambi i percorsi sono dietro un mutex. Se dieci richieste colpiscono un 401 nello stesso istante, non scatenano dieci refresh — aspettano tutte l''unico refresh in corso e poi riprovano. E quando un refresh davvero non può essere salvato, l''app fa scadere la sessione con grazia — una notifica chiara di dieci secondi "la tua sessione è scaduta, effettua di nuovo l''accesso" — invece di un calcio silenzioso alla schermata di login.
Riconnessione: aggiungila nel client, poi spostala nel backend
v1.6.9 ha reso una connessione di trascrizione caduta recuperabile invece che fatale. Quando il backend segnalava che la connessione vocale a monte era caduta, il client smetteva di mostrare un errore fatale e invece si riconnetteva — con una UX deliberatamente pacata: una singola notifica ambra "Riconnessione…" (non una per tentativo), una notifica verde "Riconnesso" quando tornava, e l''errore terminale "riavvia per favore" solo dopo che ogni tentativo era esaurito.
Poi qualcosa che mi piace di questo gruppo: una release dopo, nella v1.6.10, abbiamo cancellato quella logica di riconnessione lato client — circa 113 righe — e spostato la riconnessione nel backend, che ora emette silenziosi messaggi di stato "riconnessione" che il client si limita a riflettere. Abbiamo costruito il recupero nel client, deciso che il backend era il proprietario giusto, e l''abbiamo spostato. Non è tentennare; è il ciclo di vita onesto di un problema difficile. Una logica di affidabilità divisa tra le due estremità di una connessione è un cattivo odore — scegli un proprietario. (Il backend è alla fine diventato il proprietario definitivo di questo, che è la storia di riconnessione raccontata in perché il tuo notetaker con IA si ferma con il Wi-Fi scarso.)
La stessa release ha reso umani i limiti di tempo di registrazione: invece di un errore grezzo, una notifica di avviso che lascia continuare la registrazione, e un messaggio chiaro "Limite di Registrazione Raggiunto" quando si ferma davvero — la frase amichevole mostrata, il prefisso interno rimosso.
Un retry per domarli tutti
Entro la v1.6.11, la stessa logica di retry-con-backoff era stata copiata e incollata in tre posti diversi — auth, aggiornamenti, smontaggio della sessione. Così è stata consolidata in un singolo modulo con una sola regola condivisa su cosa valga anche solo la pena riprovare: i guasti di rete transitori (connessione resettata, timeout, singhiozzo del DNS, socket riagganciato) vengono riprovati con backoff esponenziale; un vero errore del server fallisce in fretta, perché riprovare un 4xx genuino spreca solo il tempo dell''utente. Un classificatore, alcuni preset con nome, tre chiamanti migrati su di esso.
Cosa ha insegnato questo gruppo
- Un evento
errornon gestito è un crash, non una riga di log. Node rilancia gli eventi error che non hanno un listener. Rimuovi i listener da un socket che potresti ancora star smontando, e devi riagganciare un pozzo di error no-op — o una connessione caduta si porta via l''intera app con sé. - Fai il refresh delle credenziali prima della scadenza e reattivamente su un 401 — con un mutex su entrambi. Il proattivo ti tiene loggato; il reattivo cattura i casi limite; il mutex significa che N chiamanti simultanei causano esattamente un refresh, e un tetto ai retry significa che un token scadente non può girare in loop per sempre.
- Riprova solo sugli errori transitori, e centralizza il classificatore. "Vale la pena riprovare?" è un''unica decisione che appartiene a un unico posto. Riprovare un vero errore è solo un fallimento più lento.
- Decidi a chi appartiene la riconnessione. L''abbiamo aggiunta nel client, poi spostata nel backend. Una logica di recupero che vive su entrambe le estremità è un generatore di bug; dalle una sola casa.
Questo è il terzo capitolo della storia di affidabilità che diventa GeekBye v2. Per il capitolo precedente, guarda abbiamo cancellato 5.000 righe di codice audio e le trascrizioni hanno iniziato ad apparire due volte (v1.6.0); per dove la riconnessione si è infine assestata, perché il tuo notetaker con IA si ferma con il Wi-Fi scarso; e per l''intero arco, l''anatomia del rilasciare software fino alla perfezione.