
Koneksi yang Terputus Seharusnya Tidak Menghancurkan Seluruh Aplikasimu — Tapi Aplikasi Kami Iya
Ketika backend kami offline di tengah rapat, itu bukan sekadar menjeda transkripsi — tapi menghancurkan seluruh aplikasi. Penyebabnya adalah satu event yang tak tertangani, dan perbaikannya hanya sepuluh baris. Inilah kelompok rilis yang membuat GeekBye tetap masuk dan tetap terhubung melewati hal-hal yang dulu membunuhnya.
Sebagian bug menginterupsi sebuah fitur. Yang terburuk merobohkan segala hal di sekitarnya. Pada build GeekBye awal, jika backend kami offline saat kamu sedang di tengah rapat, transkripsi bukan sekadar berhenti — seluruh aplikasi crash. Satu koneksi terputus, dan seluruh jendela lenyap.
Kelompok rilis yang memperbaiki ini — v1.6.8 hingga v1.6.11 — adalah pelajaran master tentang kerja tak glamor bernama "tetap masuk, tetap terhubung". Perbaikan utamanya sepuluh baris. Inilah apa isi sepuluh baris itu, dan semua hal lain yang dirilis bersamanya.
Sepuluh baris di antara koneksi terputus dan crash
Crash-nya hidup di kode audio. Ketika sebuah sesi transkripsi merobohkan WebSocket-nya, ia memanggil removeAllListeners() pada socket tersebut — pembersihan yang masuk akal. Tapi inilah detail Node.js yang mengubah pembersihan menjadi bencana: sebuah event error tanpa listener tidak diabaikan. Ia dilempar ulang sebagai exception yang tidak tertangkap.
Jadi bayangkan urutannya ketika backend offline: socket punya event error yang mengantre untuk dipicu. Pembersihan menjalankan removeAllListeners(), menanggalkan handler yang seharusnya menangkapnya. Sesaat kemudian error yang mengantre itu terpicu — ke ruang hampa. Node melihat event error yang tak ada penyimaknya, dan melakukan satu-satunya hal yang diizinkan kontraknya: melemparnya sebagai exception yang tidak tertangkap, yang membuat proses crash. Backend pengguna tersendat dua detik dan aplikasi mereka menghilang.
Perbaikannya (v1.6.8) nyaris antiklimaks. Tepat setelah menanggalkan listener, pasang kembali sebuah handler error no-op secara sengaja:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
Fungsi kosong itulah intinya. Ia memberi error yang tertunda tempat untuk mendarat — sebuah penyerap — sehingga Node punya listener dan tak pernah melempar ulang. Commit yang sama juga membersihkan socket setengah terbuka ketika sebuah upaya koneksi gagal di tengah jalan, sehingga koneksi yang berantakan tak bisa meninggalkan socket hidup yang menggantung dengan error yang menunggu untuk terpicu. Komentar commit-nya mengatakannya blak-blakan: "Tanpa ini, error yang tertunda menjadi exception yang tidak tertangkap dan membuat aplikasi crash." Sepuluh baris mengubah crash total menjadi koneksi terputus yang senyap dan dapat dipulihkan.
Pelajarannya lebih berharga daripada perbaikannya: setiap kali kamu memanggil removeAllListeners() pada socket atau stream yang mungkin masih kamu robohkan, pasang kembali dulu sebuah penyerap error no-op. Sebuah event error yang tak tertangani adalah crash, bukan baris log.
Tetap masuk: refresh lebih awal, dan refresh saat memantul
Paruh "tetap terhubung" punya kembaran "tetap masuk". Sebelum kelompok ini, token autentikasi GeekBye cukup kedaluwarsa saja — setelah beberapa saat kamu dibuang tanpa upacara kembali ke layar login, di tengah alur kerja, tanpa alasan yang bisa kamu lihat.
v1.6.8 memperbaiki ini dari kedua arah:
- Proaktif: sebuah timer me-refresh token sebelum kedaluwarsa, dijadwalkan pada waktu kedaluwarsa dikurangi buffer cerdas (sebagian kecil dari masa hidup token, dengan batas bawah dan atas). Kamu sudah di-refresh sebelum sempat menyadarinya.
- Reaktif: kalau sebuah permintaan tetap kembali sebagai
401— pergeseran jam, timer yang terlewat, laptop yang dingin — client me-refresh sekali dan mencoba ulang permintaan asli tepat sekali. Satu flag penjaga membatasi retry itu sehingga 401 yang persisten tak akan pernah berputar menjadi loop tak berujung.
Bagian yang halus tapi krusial: kedua jalur berada di balik sebuah mutex. Kalau sepuluh permintaan terkena 401 di saat yang sama, mereka tidak memicu sepuluh refresh — semuanya menunggu satu refresh yang sedang berjalan lalu mencoba ulang. Dan ketika sebuah refresh benar-benar tak bisa diselamatkan, aplikasi mengakhiri sesi secara mulus — sebuah toast sepuluh detik yang jelas "sesimu telah kedaluwarsa, silakan masuk lagi" — alih-alih tendangan senyap ke layar login.
Reconnect: tambahkan di client, lalu pindahkan ke backend
v1.6.9 membuat koneksi transkripsi yang terputus menjadi dapat dipulihkan alih-alih fatal. Ketika backend melaporkan bahwa koneksi ucapan di hulu telah putus, client berhenti memunculkan error fatal dan sebagai gantinya reconnect — dengan UX yang sengaja dibuat tenang: satu toast oranye "Menyambungkan ulang…" (bukan satu per upaya), sebuah toast hijau "Tersambung kembali" saat ia kembali, dan error terminal "silakan mulai ulang" hanya setelah setiap upaya habis.
Lalu ada hal yang saya suka dari kelompok ini: satu rilis kemudian, di v1.6.10, kami menghapus logika reconnect sisi client itu — sekitar 113 baris — dan memindahkan reconnect ke backend, yang kini memancarkan pesan status "menyambungkan ulang" yang tenang yang tinggal dicerminkan client. Kami membangun pemulihan di client, memutuskan bahwa backend adalah pemilik yang tepat, dan memindahkannya. Itu bukan plin-plan; itu siklus hidup yang jujur dari sebuah masalah sulit. Logika keandalan yang terbelah di kedua ujung sebuah koneksi adalah pertanda buruk — pilih satu pemilik. (Backend akhirnya menjadi pemilik definitif untuk hal ini, yang merupakan kisah reconnect yang diceritakan di mengapa pencatat rapat AI-mu berhenti saat Wi-Fi buruk.)
Rilis yang sama membuat batas waktu perekaman lebih manusiawi: alih-alih error mentah, sebuah toast peringatan yang membiarkan perekaman lanjut, dan pesan jelas "Batas Perekaman Tercapai" ketika ia benar-benar berhenti — kalimat ramah dimunculkan, prefiks internal ditanggalkan.
Satu retry untuk memerintah semuanya
Menjelang v1.6.11, logika retry-dengan-backoff yang sama telah disalin-tempel ke tiga tempat berbeda — autentikasi, pembaruan, perobohan sesi. Maka ia dikonsolidasikan menjadi satu modul dengan satu aturan bersama tentang apa yang bahkan layak dicoba ulang: kegagalan jaringan sementara (koneksi di-reset, timeout, DNS tersendat, socket putus) dicoba ulang dengan backoff eksponensial; error server yang sungguhan gagal cepat, karena mencoba ulang 4xx yang asli hanya membuang waktu pengguna. Satu classifier, beberapa preset bernama, tiga pemanggil dimigrasikan ke atasnya.
Apa yang diajarkan kelompok ini
- Sebuah event
erroryang tak tertangani adalah crash, bukan baris log. Node melempar ulang event error yang tak punya listener. Tanggalkan listener pada socket yang mungkin masih kamu robohkan, dan kamu harus memasang kembali penyerap error no-op — atau satu koneksi terputus membawa seluruh aplikasi bersamanya. - Refresh kredensial sebelum kedaluwarsa dan secara reaktif saat 401 — dengan mutex pada keduanya. Yang proaktif membuatmu tetap masuk; yang reaktif menangkap kasus tepi; mutex berarti N pemanggil bersamaan menyebabkan tepat satu refresh, dan batas retry berarti token buruk tak bisa berputar selamanya.
- Coba ulang hanya saat error sementara, dan pusatkan classifier-nya. "Apakah ini layak dicoba ulang?" adalah satu keputusan yang milik satu tempat. Mencoba ulang error yang sungguhan hanyalah kegagalan yang lebih lambat.
- Putuskan siapa pemilik reconnect. Kami menambahkannya di client, lalu memindahkannya ke backend. Logika pemulihan yang hidup di kedua ujung adalah generator bug; beri ia satu rumah.
Ini adalah bab ketiga dari kisah keandalan yang menjadi GeekBye v2. Untuk bab sebelumnya, lihat kami menghapus 5.000 baris kode audio dan transkrip mulai muncul dua kali (v1.6.0); untuk tempat reconnect akhirnya menetap, mengapa pencatat rapat AI-mu berhenti saat Wi-Fi buruk; dan untuk keseluruhan busurnya, anatomi merilis perangkat lunak hingga sempurna.