Steven
Stevenโ€ขโ€ข7 menit baca

Lahir dan Diperbaiki dalam Dua Belas Menit

Changelog GeekBye v1.8.10 bilang ia memperbaiki sebuah crash saat mengedit pintasan keyboard. Memang begitu โ€” tapi crash itu diperkenalkan dan diperbaiki dalam pull request yang sama, terpaut dua belas menit, dan tak pernah mencapai satu pun pengguna. Kisah sebenarnya adalah kaskade keandalan yang menghasilkannya: sebuah perubahan kecil dan benar tentang kapan pintasan ditangguhkan, dan bug teardown React yang jatuh dari satu baris yang dimaksudkan untuk membuat editor lebih aman.

Engineering
React
Desktop
GeekBye Releases
Lahir dan Diperbaiki dalam Dua Belas Menit

Inilah kalimat paling jujur yang bisa kutulis tentang GeekBye v1.8.10: crash yang terkenal ia perbaiki tak pernah terjadi pada siapa pun. Changelog bilang "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," yang benar, dan terbaca seperti sebuah bug yang dirilis, menggigit pengguna, dan ditambal dalam tindak lanjut. Bukan. Crash itu diperkenalkan oleh satu commit dan dihapus oleh commit lain dalam pull request yang sama, terpaut dua belas menit, di pagi Jumat yang sama. Ia hidup sepenuhnya di dalam sebuah branch. Catatan rilis menggambarkan sebuah bekas luka yang terbentuk dan sembuh sebelum siapa pun di luar repo bisa melihatnya.

Itu bukan kegagalan changelog; itu adalah bentuk jujur dari kerja iteratif, dan layak diceritakan karena dua belas menit itu memuat sebuah bug React yang sungguh-sungguh mendidik. Tapi untuk memahami crash-nya kamu harus memahami untuk apa rilis ini sebenarnya โ€” karena crash itu adalah kerusakan kolateral dari memperbaiki sesuatu yang lain.

Tentang apa rilis ini sebenarnya

GeekBye membiarkanmu mengikat ulang pintasan keyboard-nya di Pengaturan. Saat kamu merekam kombinasi baru, aplikasi harus menghentikan pintasan global-nya dari terpicu โ€” kalau tidak, menekan Cmd+B untuk menetapkannya justru akan men-toggle jendela alih-alih ditangkap. Jadi editor menangguhkan pintasan global lewat IPC: renderer memanggil window.electronAPI.setShortcutsSuspended(true), dan ShortcutsHelper milik main process merespons dengan memanggil unregisterAll(), menjatuhkan setiap registrasi globalShortcut sehingga penekanan tombol mencapai input penangkap alih-alih memicu sebuah aksi. Saat melanjutkan ia memanggil registerGlobalShortcuts() untuk mengembalikannya.

Bug yang memulai semua ini (issue #233) adalah soal cakupan penangguhan itu. Kode lama menangguhkan pintasan sepanjang waktu halaman pengaturan pintasan terbuka โ€” bukan hanya saat kamu aktif merekam sebuah tombol. Sebagian besar waktu itu tak terlihat. Tapi jika kamu membuka halaman itu, mulai melakukan sesuatu yang lain, dan menavigasi menjauh tanpa pernah menyelesaikan sebuah pengeditan, aplikasi bisa tertinggal dengan semua pintasan globalnya tak terdaftar โ€” mati diam-diam โ€” sampai kamu kembali. Commit yang memperbaikinya berterus terang soal gejalanya: menangguhkan terlalu luas berisiko "making the app appear broken if the user forgot they had the page open."

Perbaikannya adalah penyempitan satu baris: tangguhkan hanya ketika sebuah baris benar-benar sedang diedit โ€” dikunci pada !!editingShortcut, state hook yang menyimpan id baris yang kamu rekam โ€” alih-alih sepanjang kunjungan. Perubahan bagus. Tapi "hanya tangguhkan saat mengedit" memunculkan sebuah pertanyaan langsung: apa yang dihitung sebagai menyelesaikan sebuah pengeditan? Menekan sebuah tombol menyelesaikannya. Menekan Escape membatalkannya. Tapi bagaimana jika kamu hanyaโ€ฆ mengklik menjauh, atau berpindah ke aplikasi lain sepenuhnya, di tengah penangkapan? Jika tak ada yang membatalkan pengeditan, kamu tertinggal merekam sebuah pintasan ke dalam jendela yang tak terfokus, dan pintasan tetap ditangguhkan. Jadi PR yang sama menambahkan sebuah katup pengaman: batalkan pengeditan ketika fokus pergi. Dan itulah baris yang crash.

Bug dua belas menit

Katup pengaman itu adalah sebuah useEffect di ShortcutsSettings.tsx yang, selama sebuah baris sedang diedit, mendengarkan blur:

const handleBlur = () => cancelEditing()
window.addEventListener('blur', handleBlur)
inputRef.current?.addEventListener('blur', handleBlur) // the landmine

Dua listener. Satu pada window โ€” terpicu saat kamu berpindah ke aplikasi lain. Satu dilampirkan langsung, sebagai listener DOM mentah, ke <input> penangkap โ€” dimaksudkan untuk terpicu saat kamu mengklik menjauh ke elemen lain. Mereka tampak berlebihan dan tak berbahaya. Mereka bukan.

Telusuri urutannya untuk "switching windows mid-edit," yang persis adalah frasa changelog:

  1. Kamu sedang merekam sebuah pintasan; <input> terfokus. Kamu Cmd+Tab ke aplikasi lain.
  2. Blur jendela terpicu. handleBlur menjalankan cancelEditing(), yang menyetel editingShortcut kembali ke null.
  3. Menyetel state itu meng-unmount <input> โ€” baris keluar dari mode edit, jadi input dihapus dari DOM.
  4. Menghapus sebuah elemen yang terfokus dari DOM secara sinkron mengirim sebuah event blur pada elemen itu. Listener inputRef mentah menangkapnya dan memanggil cancelEditing() lagi โ€” kali ini di tengah fase commit React, saat pohon komponen sedang dirobohkan.
  5. Memanggil sebuah state setter pada sebuah fiber yang sedang di tengah teardown menyandung salah satu invarian internal React, dan ia melempar: "Should have a queue". Tampilan pengaturan crash.

Bug-nya bukan listener jendela dan bukan pula benar-benar konsep "batalkan saat blur." Melainkan bahwa salah satu dari dua listener itu adalah sebuah addEventListener mentah pada sebuah elemen yang dikelola React. React tak tahu tentang listener itu, tak membersihkannya saat unmount, dan โ€” yang krusial โ€” browser memicu blur selama penghapusan DOM yang sedang dilakukan React itu sendiri, jadi handler masuk kembali ke logika state-mu pada saat paling buruk yang mungkin. Pesan commit perbaikannya mengatakannya lebih tepat daripada yang bisa kulakukan: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle."

Perbaikannya

bf28a50, dua belas menit setelah crash-nya lahir, melakukan dua hal kecil.

Pertama, ia menghapus listener input mentah dan menyimpan hanya yang jendela, yang merupakan satu-satunya yang memang perlu menjadi sebuah listener DOM manual (tak ada React onBlur untuk "seluruh jendela kehilangan fokus"):

const handleWindowBlur = () => cancelEditing()
window.addEventListener('blur', handleWindowBlur)
return () => {
  window.removeEventListener('blur', handleWindowBlur)
}

Kedua, ia memindahkan kasus klik-menjauh โ€” "pengguna mengklik elemen lain" โ€” ke prop onBlur milik React sendiri pada input, dan menundanya satu tik:

onBlur={() => {
  // Deferred to avoid state updates during React's render cycle.
  setTimeout(() => cancelEditing(), 0)
}}

Kedua bagian itu penting. Menggunakan prop onBlur React alih-alih sebuah listener mentah berarti React memiliki siklus hidup handler dan tak akan meninggalkannya menggantung. Dan setTimeout(โ€ฆ, 0) menjamin bahwa bahkan ketika blur ini memang dipicu oleh sebuah unmount, panggilan cancelEditing() mendarat pada tik berikutnya โ€” setelah render dan commit saat ini selesai โ€” sehingga ia tak pernah bisa memperbarui state pada sebuah komponen yang masih dirobohkan. Jika komponennya sudah hilang saat itu, panggilan yang ditunda adalah no-op yang tak berbahaya.

Itulah keseluruhan perbaikannya. Hapus listener yang tak bisa dilihat React; biarkan React menjadwalkan yang tersisa.

Mengapa ini jenis membosankan yang baik

Ada godaan untuk mengarsipkan ini sebagai "sepele" โ€” sebuah setTimeout, sebuah baris yang dihapus โ€” dan melanjutkan. Tapi bentuknya adalah salah satu cara paling umum aplikasi desktop React crash, dan layak diinternalisasi:

  • Crash hanya muncul di sebuah batas siklus hidup (unmount), dipicu oleh sebuah event yang tak dijadwalkan framework (sebuah blur native selama penghapusan DOM). Ia tak akan muncul di sebuah tes klik-lewat; ia butuh jalur spesifik "fokus pergi saat mengedit."
  • Akar penyebabnya adalah mencampur dua model kepemilikan โ€” event DOM mentah dan event sintetis, terjadwal milik React โ€” pada elemen yang sama. Saat handler-mu memutasi state yang bisa meng-unmount elemen itu, jalur mentah menjadi re-entrant.
  • Perbaikannya bukan "tambahkan sebuah bendera penjaga" (meski sebuah ref isMounted akan menyembunyikannya); melainkan menghapus listener mentah sepenuhnya, yang merupakan perubahan lebih kecil dan yang benar. Pengurangan lagi.

Dan meta-pelajarannya, yang terus diputari seluruh seri ini: baca diff-nya, bukan catatan rilisnya. "Fixed a crash while editing keyboard shortcuts" akurat, tapi ia menyembunyikan bahwa crash itu ditimbulkan sendiri, di pagi yang sama, dan tak pernah dirilis โ€” dan bahwa rilis yang sebenarnya adalah kemenangan ketepatan yang tenang tentang kapan menangguhkan sebuah pintasan global.

Apa lagi yang dirilis

Dua hal lagi ikut menumpang di v1.8.10, keduanya layak satu baris. Sistem pintasan juga memperoleh penyembuhan-diri lintas tidur/bangun: macOS bisa diam-diam menjatuhkan registrasi pintasan global sebuah aplikasi ketika mesin tidur, jadi sebuah handler resume kini memverifikasi dan mendaftarkannya ulang, yang berarti pintasanmu masih bekerja setelah laptop bangun tanpa peluncuran ulang. Dan di sisi AI, klien menjadi lebih pintar soal batas laju โ€” permintaan streaming yang menabrak sebuah 429 kini mundur secara eksponensial dan mereset penghitungnya ketika kamu berhenti merekam, alih-alih menunggu sebuah penundaan tetap. Tiga perbaikan keandalan pada subsistem yang berbeda, salah satunya adalah crash yang tak pernah menjadi crash.

Untuk bab sebelumnya dalam kisah v1, mencetak rapat ke PDF tanpa library PDF (v1.8.9); dan untuk keseluruhan alurnya, anatomi merilis software sampai sempurna.

Artikel Terkait

Mencetak Rapat ke PDF Tanpa Library PDF
Steven
Stevenโ€ขโ€ข9 menit baca

Mencetak Rapat ke PDF Tanpa Library PDF

GeekBye mengekspor sebuah rapat sebagai PDF, dan tak ada library PDF di mana pun dalam kodenya. Ia me-render HTML di sebuah jendela browser yang tak terlihat dan mencetaknya. Pilihan itu adalah keseluruhan kisahnya: ia membuat fitur ini mudah dibangun dan memberinya setiap kegagalan yang dimiliki sebuah browser sungguhan โ€” sebuah kilatan putih, sebuah batas panjang URL, dan sebuah page break yang memotong screenshot menjadi dua. Perbaikan untuk bug paling jelek adalah satu baris CSS.

Engineering
Electron
Desktop
Dari OCR ke Piksel Tanpa Kehilangan Fallback
Steven
Stevenโ€ขโ€ข7 menit baca

Dari OCR ke Piksel Tanpa Kehilangan Fallback

GeekBye berhenti membaca screenshot-mu dengan OCR dan mulai mengirim gambar sebenarnya ke model vision โ€” yang merupakan bagian mudahnya. Bagian yang memberi pelajaran adalah optimisasi yang melewati OCR sepenuhnya untuk pengguna vision, diam-diam mematahkan fallback ke model non-vision, dan harus ditarik kembali ke sebuah versi di mana jaring pengaman berjalan paralel dan tak berbiaya apa pun.

Engineering
AI
Desktop
Dua Mode Kegagalan Sebuah Overlay Click-Through
Steven
Stevenโ€ขโ€ข8 menit baca

Dua Mode Kegagalan Sebuah Overlay Click-Through

Jendela GeekBye mengambang di atas segalanya dan membiarkan klikmu menembusnya โ€” kecuali di tempat ia punya tombol. Itu kontrak dua sisi, dan v1.8.5 serta v1.8.14 adalah wujudnya ketika masing-masing sisi patah: satu release di mana overlay menelan sebuah dialog sistem, satu lagi di mana ia mencuri ketikan tombolmu. Perbaikan yang menang untuk yang kedua adalah menghapus kode.

Engineering
Electron
Desktop