Steven
Steven7 perc olvasás

Megszületett és megjavult tizenkét perc alatt

A GeekBye v1.8.10 changelogja azt mondja, hogy megjavított egy összeomlást a billentyűparancsok szerkesztése közben. Meg is javította — de az összeomlást ugyanabban a pull requestben vezették be és javították meg, tizenkét perc különbséggel, és soha nem jutott el egyetlen felhasználóhoz sem. Az igazi történet a megbízhatósági kaszkád, amely előállította: egy kicsi, helyes változtatás abban, hogy mikor függesztődnek fel a parancsok, és a React-lebontási bug, amely egyetlen sorból esett ki, amelynek a szerkesztőt biztonságosabbá kellett volna tennie.

Mérnöki munka
React
Desktop
GeekBye kiadások
Megszületett és megjavult tizenkét perc alatt

Íme a legőszintébb mondat, amit a GeekBye v1.8.10-ről írhatok: az összeomlás, amelynek a javításáról híres, soha nem történt meg senkivel. A changelog azt mondja, "Fixed a crash while editing keyboard shortcuts and switching windows mid-edit," ami igaz, és úgy olvasható, mint egy bug, amely kiszállt, megharapta a felhasználókat, és egy későbbi kiadásban foltozták be. Nem így volt. Az összeomlást egy commit vezette be, és egy másik törölte ugyanabban a pull requestben, tizenkét perc különbséggel, ugyanazon a péntek reggelen. Teljes egészében egy branchen belül élt. A kiadási jegyzetek egy heget írnak le, amely azelőtt képződött és gyógyult be, hogy a repón kívül bárki láthatta volna.

Ez nem a changelog kudarca; ez az iteratív munka őszinte alakja, és érdemes elmesélni, mert a tizenkét perc egy valóban tanulságos React-bugot tartalmaz. De ahhoz, hogy megértsd az összeomlást, meg kell értened, hogy miről szólt valójában a kiadás — mert az összeomlás valami más javításának járulékos kára volt.

Miről szólt valójában a kiadás

A GeekBye lehetővé teszi, hogy a Beállításokban átkösd a billentyűparancsait. Miközben egy új kombinációt rögzítesz, az alkalmazásnak meg kell akadályoznia a globális parancsainak elsülését — különben a Cmd+B megnyomása a hozzárendeléshez egyszerűen átváltaná az ablakot, ahelyett, hogy rögzítenék. Így a szerkesztő IPC-n keresztül felfüggeszti a globális parancsokat: a renderer meghívja a window.electronAPI.setShortcutsSuspended(true)-t, és a fő folyamat ShortcutsHelper-je az unregisterAll() meghívásával válaszol, elejtve minden globalShortcut regisztrációt, hogy a billentyűlenyomás a rögzítő inputot érje el, ahelyett, hogy egy műveletet indítana. Folytatáskor meghívja a registerGlobalShortcuts()-t, hogy visszategye őket.

A bug, amely mindezt elindította (issue #233), a felfüggesztés hatóköréről szólt. A régi kód a parancsokat a teljes időre felfüggesztette, amíg a parancsok beállítási oldala nyitva volt — nem csak amíg aktívan rögzítettél egy billentyűt. Az esetek nagy részében ez láthatatlan. De ha megnyitottad az oldalt, elkezdtél valami mást csinálni, és elnavigáltál anélkül, hogy valaha befejeztél volna egy szerkesztést, az alkalmazás az összes globális parancsával regisztrálatlanul maradhatott — csendben holtan — amíg vissza nem tértél. A commit, amely megjavítja, őszinte a tünettel kapcsolatban: a túl széles felfüggesztés azt kockáztatta, hogy "making the app appear broken if the user forgot they had the page open."

A javítás egy egysoros szűkítés: csak akkor függessz fel, amikor egy sort ténylegesen szerkesztenek — a !!editingShortcut-ra kulcsolva, a hook állapotára, amely a rögzített sor id-jét tartja — a teljes látogatás helyett. Jó változtatás. De a „csak szerkesztés közben függessz fel" azonnali kérdést vet fel: mi számít egy szerkesztés befejezésének? Egy billentyű megnyomása befejezi. Az Escape megnyomása megszakítja. De mi van, ha csak… elkattintasz, vagy teljesen átváltasz egy másik alkalmazásra, a rögzítés közepén? Ha semmi nem szakítja meg a szerkesztést, ott maradsz egy parancsot rögzítve egy ablakba, amely nincs fókuszban, és a parancsok felfüggesztve maradnak. Így ugyanaz a PR hozzáadott egy biztonsági szelepet: szakítsd meg a szerkesztést, amikor a fókusz elhagyja. És ez az a sor, amely összeomlott.

A tizenkét perces bug

A biztonsági szelep egy useEffect volt a ShortcutsSettings.tsx-ben, amely, miközben egy sort szerkesztettek, blurre figyelt:

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

Két listener. Egy a window-on — akkor sül el, amikor átváltasz egy másik alkalmazásra. Egy közvetlenül, raw DOM listenerként csatolva a rögzítő <input>-ra — arra szánva, hogy akkor süljön el, amikor elkattintasz egy másik elemre. Redundánsnak és ártalmatlannak tűnnek. Nem azok.

Járd végig a szekvenciát a "switching windows mid-edit"-re, ami pontosan a changelog kifejezése:

  1. Egy parancsot rögzítesz; a <input> fókuszban van. Cmd+Tab-ol egy másik alkalmazásra.
  2. Az ablak blurje elsül. A handleBlur lefuttatja a cancelEditing()-et, amely a editingShortcut-ot visszaállítja null-ra.
  3. Ennek az állapotnak a beállítása leszereli a <input>-ot — a sor elhagyja a szerkesztési módot, így az input eltávolítódik a DOM-ból.
  4. Egy fókuszált elem eltávolítása a DOM-ból szinkron módon elküld egy blur eseményt azon az elemen. A raw inputRef listener elkapja, és újra meghívja a cancelEditing()-et — ezúttal a React commit fázisának közepén, miközben a komponensfa lebomlik.
  5. Egy állapotbeállító meghívása egy fiberen, amely a lebontás közepén van, megbotlik a React egyik belső invariánsában, és eldobja: "Should have a queue". A beállítások nézet összeomlik.

A bug nem az ablak listener, és nem is igazán a „megszakítás bluron" koncepciója. Az a baj, hogy a két listener egyike egy raw addEventListener volt egy React által kezelt elemen. A React nem tud arról a listenerről, nem takarítja el unmountkor, és — ami döntő — a böngésző a blurt éppen a DOM-eltávolítás során süti el, amelyet a React végez, így a handler a legrosszabb lehetséges pillanatban lép újra be az állapotlogikádba. A javító commit üzenete pontosabban mondja, mint én: "when cancelEditing unmounts the input, the blur fires and calls cancelEditing again during React's render cycle."

A javítás

A bf28a50, tizenkét perccel az összeomlás megszületése után, két apró dolgot csinál.

Először, törli a raw input listenert, és csak az ablakét tartja meg, amely az egyetlen, amelynek egyáltalán manuális DOM listenernek kellett lennie (nincs React onBlur arra, hogy „az egész ablak elvesztette a fókuszt"):

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

Másodszor, a kattints-el esetet — „a felhasználó egy másik elemre kattintott" — a React saját onBlur propjára helyezi az inputon, és egy tickkel elhalasztja:

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

Mindkét rész számít. A React onBlur propjának használata raw listener helyett azt jelenti, hogy a React birtokolja a handler életciklusát, és nem hagyja lógni. A setTimeout(…, 0) pedig garantálja, hogy még amikor ezt a blurt egy unmount váltja ki, a cancelEditing() hívás a következő tickre landol — miután a jelenlegi render és commit befejeződött — így soha nem frissítheti egy olyan komponens állapotát, amely még lebomlik. Ha a komponens addigra már eltűnt, az elhalasztott hívás egy ártalmatlan no-op.

Ez az egész javítás. Töröld a listenert, amelyet a React nem látott; hagyd, hogy a React ütemezze azt, amely megmaradt.

Miért ez a jó fajta unalom

Van egy kísértés, hogy ezt a „triviális" alá sorold — egy setTimeout, egy törölt sor — és továbblépj. De az alakja az egyik leggyakoribb módja annak, ahogy a React desktop alkalmazások összeomlanak, és érdemes elsajátítani:

  • Az összeomlás csak egy életciklus-határon (unmount) jelenik meg, amelyet egy esemény vált ki, amelyet a keretrendszer nem ütemez (egy natív blur a DOM-eltávolítás során). Nem fog megjelenni egy click-through tesztben; szüksége van a specifikus „a fókusz szerkesztés közben elhagyja" útvonalra.
  • A gyökérok két tulajdonlási modell keverése — raw DOM események és a React szintetikus, ütemezett eseményei — ugyanazon az elemen. Abban a pillanatban, amikor a handlered olyan állapotot mutál, amely leszerelheti azt az elemet, a raw útvonal újrabelépővé válik.
  • A javítás nem az, hogy „adj hozzá egy védőjelzőt" (bár egy isMounted ref elfedte volna); az a raw listener teljes eltávolítása, amely a kisebb változtatás és a helyes. Kivonás megint.

És a meta-lecke, az, amely körül ez az egész sorozat kering: olvasd a diffet, ne a kiadási jegyzetet. A "Fixed a crash while editing keyboard shortcuts" pontos, de elrejti, hogy az összeomlás önokozott volt, ugyanazon a reggelen, és soha nem került ki — és hogy a valódi kiadás egy csendes helyességi győzelem arról, hogy mikor függessz fel egy globális parancsot.

Mi más került ki

Még két dolog utazott vele a v1.8.10-ben, mindkettő megér egy sort. A parancsrendszer önjavítást is kapott alvás/ébredés során: a macOS csendben elejtheti egy alkalmazás globális parancs-regisztrációit, amikor a gép alszik, így egy folytatás-handler most újra ellenőrzi és újra regisztrálja őket, ami azt jelenti, hogy a parancsaid továbbra is működnek, miután a laptop felébred, újraindítás nélkül. Az AI oldalon pedig a kliens okosabb lett a rate limitekkel kapcsolatban — a streaming kérések, amelyek 429-be ütköznek, most exponenciálisan hátrálnak, és nullázzák a számlálójukat, amikor leállítod a rögzítést, ahelyett, hogy egy fix késleltetést kivárnának. Három megbízhatósági javítás különböző alrendszerekben, amelyek egyike az összeomlás, amely soha nem volt összeomlás.

A v1-történet előző fejezetéért lásd egy megbeszélés PDF-be nyomtatása PDF-könyvtár nélkül (v1.8.9); a teljes ívért pedig a szoftver tökéletesre szállításának anatómiája.

Kapcsolódó cikkek

Egy megbeszélés PDF-be nyomtatása PDF-könyvtár nélkül
Steven
Steven8 perc olvasás

Egy megbeszélés PDF-be nyomtatása PDF-könyvtár nélkül

A GeekBye egy megbeszélést PDF-ként exportál, és sehol a kódban nincs PDF-könyvtár. HTML-t renderel egy láthatatlan böngészőablakban, és azt nyomtatja ki. Ez a döntés az egész történet: könnyűvé tette a funkció megépítését, és megadott neki minden hibát, ami egy valódi böngészőnek van — egy fehér felvillanás, egy URL-hosszkorlát és egy oldaltörés, amely félbevágta a képernyőképeket. A legrondább bug javítása egyetlen sor CSS volt.

Mérnöki munka
Electron
Desktop
Az OCR-től a pixelekig, a fallback elvesztése nélkül
Steven
Steven7 perc olvasás

Az OCR-től a pixelekig, a fallback elvesztése nélkül

A GeekBye abbahagyta, hogy OCR-rel olvassa a képernyőképeidet, és elkezdte az igazi képet küldeni a vision modelleknek — ami a könnyű rész. A tanulságos rész az az optimalizálás, amely a vision felhasználóknál teljesen kihagyta az OCR-t, csendben eltörte a nem-vision modellekhez tartozó fallbacket, és vissza kellett vonni egy olyan verzióra, amelyben a biztonsági háló párhuzamosan fut, és semmibe sem kerül.

Mérnöki munka
AI
Desktop
A click-through overlay két hibamódja
Steven
Steven8 perc olvasás

A click-through overlay két hibamódja

A GeekBye ablaka mindenek fölött lebeg, és átengedi rajta a kattintásaidat — kivéve ott, ahol gombjai vannak. Ez egy kétoldalú szerződés, és a v1.8.5 meg a v1.8.14 azt mutatja, hogy néz ki, amikor az egyes oldalak eltörnek: az egyik kiadásban az overlay elnyelt egy rendszerablakot, a másikban ellopta a billentyűleütéseidet. A második győztes javítása kódtörlés volt.

Mérnöki munka
Electron
Desktop