
Ako streamovať report naživo bez blikania
Keď sa stretnutie skončí, zhrnutie GeekBye sa teraz dopĺňa naživo namiesto toho, aby ste zízali na točiaci sa indikátor. Aby streamovacie rozhranie pôsobilo pokojne, a nie roztrasene, bolo treba vyriešiť blikanie dvakrát — raz pre štruktúrované polia, raz pre markdown — a druhým riešením bol renderer, ktorý sme už mali.
Pred týmto vydaním znamenalo ukončenie stretnutia v GeekBye zízanie na točiaci sa indikátor. Aplikácia poslala celú vašu transkripciu na analýzu, čakala na celé zhrnutie — skóre, kľúčové body, úlohy, všetko — a potom to hodila na obrazovku naraz. Fungovalo to, ale pôsobilo to pomaly, pretože ste zízali do prázdna, kým to pracovalo.
GeekBye v1.6.13–v1.6.15 to premenil na report, ktorý prúdi naživo, pole po poli, ako sa generuje. Zaujímavá časť nie je samotné streamovanie — je to všetko, čo sme museli urobiť, aby streamovanie nevyzeralo roztrasene. Pokojné streamovanie a kostrbaté streamovanie sú tá istá funkcia s veľmi odlišným prevedením.
Blikanie, časť prvá: nenechaj každú dávku mlátiť Reactom
Zhrnutie nie je jeden blok — sú to štruktúrované polia (celkové skóre, kľúčové body, úlohy a tak ďalej), ktoré backend vydáva po jednom cez server-sent events. Naivný spôsob, ako to renderovať, je aktualizovať stav Reactu pri každej udalosti, ako prichádza.
Urob to a dostaneš blikanie. Každé prichádzajúce pole spustí vlastný opätovný render; komponenty, ktoré ukazovali „prázdne", sa namontujú, potom odmontujú, potom znova namontujú ako „naplnené"; rozloženie sa mykne pri každom pakete. Report sa skladá sám pred vašimi očami tým najhorším možným spôsobom — viditeľne, nervózne.
Oprava je dvojdielna disciplína. Po prvé, akumuluj prichádzajúce polia v ref — meniteľnom držiaku, ktorý nespúšťa rendery — a publikuj do stavu Reactu raz za dávku s čerstvou kópiou, aby sa strom aktualizoval zámerne, nie pri každej mikroudalosti. Po druhé, renderuj jeden stabilný strom komponentov po celý čas, či už pri streamovaní alebo po dokončení, so zástupnými symbolmi na mieste pre polia, ktoré ešte neprišli:
// 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>
Komponent, ktorý ukazuje skóre, sa nikdy neodmontuje — len ukazuje …, kým číslo nepristane, a potom číslo. Nič sa nemykne. A plne poskladaný objekt sa nakoniec aj tak nakešuje do lokálnej databázy, takže streamovacie rozhranie a trvalé úložisko koexistujú bez boja.
Blikanie, časť druhá: renderer, ktorý sme už mali
Druhé blikanie je jemnejšie a žije v samotnom markdown. Report renderuje bohatý markdown — nadpisy, tučné písmo, zoznamy, bloky kódu. Ale markdown prichádzajúci uprostred streamu je v ktoromkoľvek okamihu nedokončený. Zoznam s jednou odrážkou zatiaľ. Blok kódu, ktorý sa otvoril, ale nezavrel. Značka tučného bez partnera zatiaľ.
Konvenčný renderer markdown znovu parsuje celý reťazec pri každom tokene, a tento nedokončený stav sa zakaždým naparsuje na niečo iné — takže zoznam bliká, blok kódu blysne otvorením a zatvorením, rozloženie skáče, ako sa tokeny dopĺňajú. Je to tá istá nervózna montáž ako predtým, o vrstvu nižšie.
Oprava bola takmer trápne dostupná: už sme používali renderer markdown vedomý streamovania pre živý AI chat. Renderer postavený tak, aby toleroval neúplné tokeny — aby renderoval nedokončený markdown stabilne a usadil ho len v priebehu dopĺňania tokenov — namiesto toho, aby zakaždým parsoval od nuly. Report potreboval len použiť ten istý. Presne tento problém sme vyriešili pre chat mesiace predtým; „opravou" pre report bolo uvedomiť si, že nástroj už máme, a namieriť ho na druhú plochu. Opätovné použitie zdolalo prestavbu.
Bonus: formátované kopírovanie je problém formátu schránky
Keď report začal dobre vyzerať, ľudia ho chceli vkladať — do dokumentu, do e-mailu — a zachovať formátovanie a snímky obrazovky. Inštinkt je serializovať report do reťazca markdown a dúfať, že cieľ ho vyrenderuje. Zvyčajne to neurobí.
Skutočná odpoveď je, že schránka drží viacero reprezentácií naraz. Tak zapíšeme dve: HTML verziu so zachovaným formátovaním a snímkami obrazovky vloženými ako inline obrázky, a textovú zálohu pre ciele, ktoré neberú HTML. Vlož do formátovaného editora a dostaneš naformátovaný report s obrázkami; vlož do obyčajného textového poľa a dostaneš čistý text. „Formátované kopírovanie" nikdy nebolo problémom serializácie — bol to problém vyber-správny-formát-schránky.
To isté vydanie dalo reportu aj štítky Ja / Oni v štýle chatu s dvomi hovoriacimi zarovnanými na opačné strany, aby sa transkripcia čítala ako rozhovor, ktorým bola, a presunulo vaše vlastné snímky obrazovky na ich stranu, aby to sadlo. (Jedno vydanie v tomto okne, v1.6.14, bola čistá prestavba — žiaden príbeh tu nie je, a je poctivé to povedať.)
Tri veci, ktoré nás streamovacie rozhranie naučilo
- Bufferuj streamované polia, publikuj raz za dávku. Nechať každú server-sent event poháňať vlastnú aktualizáciu stavu je to blikanie. Akumulátor v ref plus jediná publikácia na dávku, do stromu, ktorý sa nikdy neodmontuje, mení nervóznu montáž na pokojné dopĺňanie.
- Čokoľvek, čo streamuje, potrebuje renderer vedomý streamovania. Renderer, ktorý znovu parsuje celý reťazec pri každom tokene, bude blikať na nedokončenom markdown. Použi taký postavený pre neúplný vstup — a ak už jeden máš pre inú plochu, znovu ho použi, skôr než postavíš druhý.
- Formátované kopírovanie je o formátoch schránky, nie o serializácii. Zapíš
text/htmlso vloženými obrázkami aj zálohutext/plain. Schránka bola navrhnutá niesť oboje; využi to.
Toto je piata kapitola príbehu o spoľahlivosti a vypilovaní, ktorý sa stáva GeekBye v2. Za predchádzajúcou kapitolou pozri bug so slúchadlami do uší (v1.6.12); za tým, kde sa tá istá myšlienka server-sent events neskôr stala celým záložným transportom, transkripcia naživo, keď firewall blokuje WebSockets (v2.0.8); a za celým oblúkom anatómiu doručenia softvéru do dokonalosti.