Steven
Steven5 min čtení

Jak streamovat živý report bez blikání

Když vaše schůzka skončí, souhrn od GeekBye se teď doplňuje živě, místo aby vás nutil zírat na točící se kolečko. Aby streamované rozhraní působilo klidně, a ne trhaně, jsme museli vyřešit blikání dvakrát — jednou u strukturovaných polí, jednou u markdownu — a druhá oprava byl renderer, který jsme už měli.

Inženýrství
Frontend
Streamování
Vydání GeekBye
Jak streamovat živý report bez blikání

Před tímto vydáním znamenalo ukončení schůzky v GeekBye zírání na točící se kolečko. Aplikace odeslala celý váš přepis k analýze, počkala na celý souhrn — skóre, klíčové body, akční položky, celý balík — a pak ho na obrazovku vysypala najednou. Funkční, ale působilo to pomale, protože jste zatímco pracovala zírali do prázdna.

GeekBye v1.6.13–v1.6.15 to proměnilo v report, který se streamuje živě, pole po poli, tak jak se generuje. Zajímavá část není streamování — je to všechno, co jsme museli udělat, aby streamování nevypadalo trhaně. Klidné streamování a rozklepané streamování jsou tatáž funkce s velmi odlišným provedením.

Blikání, část první: nenech každý chunk lomcovat Reactem

Souhrn není jeden blok — jsou to strukturovaná pole (celkové skóre, klíčové body, akční položky a tak dále), která backend vysílá po jednom přes server-sent events. Naivní způsob, jak to renderovat, je aktualizovat stav Reactu při každé příchozí události.

Udělejte to a dostanete blikání. Každé příchozí pole spustí své vlastní překreslení; komponenty, které zobrazovaly „prázdno", se namontují, pak odmontují, pak znovu namontují jako „načtené"; rozvržení sebou při každém paketu cukne. Report se skládá sám před vámi tím nejhorším možným způsobem — viditelně, nervózně.

Oprava je dvoudílná disciplína. Zaprvé, akumulujte příchozí pole v refu — proměnlivém držáku, který nespouští překreslení — a publikujte do stavu Reactu jednou za chunk s čerstvou kopií, aby se strom aktualizoval záměrně, ne při každé mikroudálosti. Zadruhé, renderujte jeden stabilní strom komponent po celou dobu, ať už streamuje, nebo je hotovo, s inline zástupnými znaky pro pole, která ještě nedorazila:

// accumulate in a ref, publish once per chunk
partialRef.current = { ...partialRef.current, [field]: value }
setPartial({ ...partialRef.current })

// one tree, never swapped; missing fields show a placeholder in place
const report = isStreaming ? partial : saved
<Score>{report.overallScore ?? '…'}</Score>

Komponenta zobrazující skóre se nikdy neodmontuje — jen ukazuje , dokud číslo nepřistane, a pak číslo. Nic sebou necukne. A plně složený objekt se nakonec stále ukládá do mezipaměti v lokální databázi, takže streamované rozhraní a trvalé úložiště koexistují, aniž by spolu bojovaly.

Blikání, část druhá: renderer, který jsme už měli

Druhé blikání je jemnější a žije v samotném markdownu. Report renderuje formátovaný markdown — nadpisy, tučné písmo, seznamy, bloky kódu. Ale markdown přicházející uprostřed streamu je v každém daném okamžiku nedokončený. Seznam s jednou odrážkou zatím. Blok kódu, který byl otevřen, ale ne uzavřen. Značka tučného písma, která zatím nemá partnera.

Konvenční renderer markdownu znovu parsuje celý řetězec při každém tokenu, a ten nedokončený stav se pokaždé zparsuje na něco jiného — takže seznam bliká, blok kódu problikává otevřený a zavřený, rozvržení skáče, jak se tokeny dokončují. Je to tatáž nervózní montáž jako předtím, o vrstvu níž.

Oprava byla téměř trapně po ruce: už jsme používali renderer markdownu vědomý si streamingu pro živý AI chat. Renderer postavený tak, aby toleroval neúplné tokeny — aby renderoval nedokončený markdown stabilně a usadil ho, až se tokeny dokončí — místo aby pokaždé parsoval od nuly. Report jen potřeboval použít ten samý. Přesně tenhle problém jsme pro chat vyřešili o měsíce dřív; „opravou" pro report bylo rozpoznat, že už ten nástroj máme, a namířit ho na druhou plochu. Znovupoužití porazilo přestavbu.

Bonus: formátované kopírování je problém formátu schránky

Jakmile report vypadal dobře, lidé ho chtěli vložit — do dokumentu, do e-mailu — a zachovat formátování i snímky obrazovky. Instinkt je serializovat report do markdownového řetězce a doufat, že ho cíl vyrenderuje. Obvykle to neudělá.

Skutečná odpověď je, že schránka drží více reprezentací najednou. Tak zapíšeme dvě: HTML verzi s neporušeným formátováním a snímky obrazovky vloženými jako inline obrázky, a záložní prostý text pro cíle, které HTML nepřijmou. Vložte do formátovaného editoru a dostanete zformátovaný report s obrázky; vložte do pole s prostým textem a dostanete čistý text. „Formátované kopírování" nikdy nebylo problémem serializace — byl to problém výběru-správného-formátu-schránky.

Totéž vydání dalo reportu i chatové štítky Já / Oni se dvěma mluvčími zarovnanými na opačné strany, aby se přepis četl jako konverzace, kterou byl, a přesunulo vaše vlastní snímky obrazovky na jejich stranu, aby to sedělo. (Jedno vydání v tomto okně, v1.6.14, byla čistá přestavba — žádný příběh tam není, a je poctivé to říct.)

Tři věci, které nás streamované rozhraní naučilo

  1. Bufferuj příchozí pole, publikuj jednou za chunk. Nechat každou server-sent event řídit vlastní aktualizaci stavu je to blikání. Ref akumulátor plus jediná publikace za chunk, do stromu, který se nikdy neodmontuje, promění nervózní montáž v klidné vyplňování.
  2. Cokoli, co streamuje, potřebuje renderer vědomý si streamingu. Renderer, který při každém tokenu znovu parsuje celý řetězec, bude na nedokončeném markdownu blikat. Použij takový, který je postaven pro neúplný vstup — a pokud už jeden pro jinou plochu máš, znovu ho použij, než postavíš druhý.
  3. Formátované kopírování je o formátech schránky, ne o serializaci. Zapiš text/html s vloženými obrázky a záložní text/plain. Schránka byla navržena tak, aby nesla oboje; využij to.

Tohle je pátá kapitola příběhu o spolehlivosti a vyladění, který se stává GeekBye v2. Pro předchozí kapitolu viz chyba se sluchátky (v1.6.12); pro to, kde se stejný nápad se server-sent-events později stal celým záložním transportem, živý přepis, když firewall blokuje WebSockety (v2.0.8); a pro celý oblouk anatomie dodávání softwaru k dokonalosti.

Související články

Přerušené spojení by nemělo shodit celou aplikaci — ale ta naše to dělala
Steven
Steven6 min čtení

Přerušené spojení by nemělo shodit celou aplikaci — ale ta naše to dělala

Když nám backend spadl uprostřed schůzky, nezastavil jen přepis — shodil celou aplikaci. Příčinou byla jediná neošetřená událost a oprava měla deset řádků. Tohle je shluk vydání, díky kterému GeekBye zůstává přihlášený a připojený i skrz věci, které ho dřív zabíjely.

Inženýrství
Spolehlivost
Electron
Vaše Mac aplikace získá přístup k mikrofonu — a pak ho při každém spuštění zapomene
Steven
Steven5 min čtení

Vaše Mac aplikace získá přístup k mikrofonu — a pak ho při každém spuštění zapomene

GeekBye požádal o oprávnění k mikrofonu, udělili jste ho a fungovalo to. Při dalším spuštění: pryč. A aplikace se v Nastavení systému → Mikrofon vůbec neobjevila. Viníkem byla bezpečnostní funkce macOS, která aplikaci tiše spouštěla z cesty, jež mizí — tady je diagnóza a oprava pomocí jediné výzvy.

macOS
Oprávnění
Inženýrství
Jedna proměnná CSS, pět kol revize a Swift toolchain, který lhal
Steven
Steven6 min čtení

Jedna proměnná CSS, pět kol revize a Swift toolchain, který lhal

GeekBye v2.0.7 udělal celý overlay nastavitelně průsvitným — což zní jako jednořádková změna v CSS a rozhodně nebylo. Skutečný příběh je to, co odhalila revize kódu: dvě plochy, které odmítaly zprůhlednět, nahrávací lišta, jež vypadala špatně při stejné neprůhlednosti, a zkompilovaná binárka, která poskočila o 672 bajtů, protože naše vlastní dokumentace řekla revidujícímu špatnou verzi Swiftu.

Inženýrství
Design
Build