Steven
Steven5 min czytania

Jak transmitować raport na żywo bez migotania

Gdy kończysz spotkanie, podsumowanie GeekBye teraz wypełnia się na żywo, zamiast zmuszać cię do wpatrywania się w kręcący się wskaźnik. Sprawienie, by interfejs strumieniowy sprawiał wrażenie spokojnego, a nie nerwowego, wymagało rozwiązania migotania dwukrotnie — raz dla pól strukturalnych, raz dla markdown — a drugą naprawą był renderer, który już mieliśmy.

Inżynieria
Frontend
Streaming
Wydania GeekBye
Jak transmitować raport na żywo bez migotania

Przed tym wydaniem zakończenie spotkania w GeekBye oznaczało wpatrywanie się w kręcący się wskaźnik. Aplikacja wysyłała całą twoją transkrypcję do analizy, czekała na całe podsumowanie — ocenę, kluczowe punkty, zadania do wykonania, wszystko — a potem zrzucała to na ekran naraz. Działało, ale sprawiało wrażenie powolnego, bo wpatrywałeś się w nic, gdy pracowało.

GeekBye v1.6.13–v1.6.15 zmienił to w raport, który wpływa na żywo, pole po polu, w miarę jak jest generowany. Ciekawa część to nie strumieniowanie — to wszystko, co musieliśmy zrobić, by strumieniowanie nie wyglądało nerwowo. Spokojne strumieniowanie i toporne strumieniowanie to ta sama funkcja o bardzo różnym wykonaniu.

Migotanie, część pierwsza: nie pozwól, by każdy kawałek szarpał Reactem

Podsumowanie to nie jeden blok — to pola strukturalne (ogólna ocena, kluczowe punkty, zadania do wykonania i tak dalej), które backend emituje pojedynczo przez server-sent events. Naiwny sposób renderowania tego to aktualizacja stanu Reacta przy każdym zdarzeniu, gdy nadchodzi.

Zrób tak, a dostaniesz migotanie. Każde nadchodzące pole wyzwala własny ponowny render; komponenty pokazujące „puste" montują się, potem odmontowują, potem znów montują jako „wypełnione"; układ drga przy każdym pakiecie. Raport składa się sam na twoich oczach w najgorszy możliwy sposób — widocznie, nerwowo.

Naprawa to dwuczęściowa dyscyplina. Po pierwsze, akumuluj nadchodzące pola w ref — mutowalnym uchwycie, który nie wyzwala renderów — i publikuj do stanu Reacta raz na kawałek ze świeżą kopią, tak by drzewo aktualizowało się z rozmysłem, a nie przy każdym mikro-zdarzeniu. Po drugie, renderuj jedno stabilne drzewo komponentów przez cały czas, czy to podczas strumieniowania, czy po zakończeniu, z placeholderami w miejscu dla pól, które jeszcze nie dotarły:

// 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 pokazujący ocenę nigdy się nie odmontowuje — po prostu pokazuje , dopóki liczba nie wyląduje, a potem liczbę. Nic nie drga. A w pełni złożony obiekt na końcu i tak trafia do lokalnej bazy danych, więc strumieniowany interfejs i trwałe przechowywanie współistnieją bez walki.

Migotanie, część druga: renderer, który już mieliśmy

Drugie migotanie jest subtelniejsze i żyje w samym markdown. Raport renderuje bogaty markdown — nagłówki, pogrubienia, listy, bloki kodu. Ale markdown nadchodzący w trakcie strumienia jest, w każdej danej chwili, niedokończony. Lista z jednym punktem jak dotąd. Blok kodu, który został otwarty, ale nie zamknięty. Znacznik pogrubienia bez jeszcze pary.

Konwencjonalny renderer markdown re-parsuje cały ciąg przy każdym tokenie, a ten niedokończony stan parsuje się za każdym razem na coś innego — więc lista miga, blok kodu błyska otwarciem i zamknięciem, układ skacze, gdy tokeny się kończą. To to samo nerwowe składanie co wcześniej, jedną warstwę niżej.

Naprawa była niemal krępująco dostępna: już używaliśmy renderera markdown świadomego strumieniowania do czatu AI na żywo. Renderera zbudowanego, by tolerować niedokończone tokeny — by renderować niedokończony markdown stabilnie i osadzić go dopiero, gdy tokeny się dopełnią — zamiast re-parsować od zera za każdym razem. Raport potrzebował po prostu użyć tego samego. Rozwiązaliśmy dokładnie ten problem dla czatu miesiące wcześniej; „naprawą" dla raportu było uświadomienie sobie, że już mamy narzędzie, i skierowanie go na drugą powierzchnię. Ponowne użycie pobiło przebudowę.

Bonus: kopiowanie sformatowane to problem formatu schowka

Gdy raport już dobrze wyglądał, ludzie chcieli go wkleić — do dokumentu, do maila — i zachować formatowanie oraz zrzuty ekranu. Instynkt to zserializować raport do ciągu markdown i mieć nadzieję, że cel go wyrenderuje. Zwykle tego nie robi.

Prawdziwa odpowiedź jest taka, że schowek trzyma wiele reprezentacji naraz. Więc zapisujemy dwie: wersję HTML z nietkniętym formatowaniem i zrzutami ekranu osadzonymi jako obrazy inline oraz tekstowy fallback dla celów, które nie przyjmują HTML. Wklej do edytora sformatowanego, a dostaniesz sformatowany raport z obrazkami; wklej do zwykłego pola tekstowego, a dostaniesz czysty tekst. „Kopiowanie sformatowane" nigdy nie było problemem serializacji — to był problem wybierz-właściwy-format-schowka.

To samo wydanie dało też raportowi etykiety Ja / Oni w stylu czatu z dwoma mówcami wyrównanymi do przeciwnych stron, tak by transkrypcja czytała się jak rozmowa, którą była, i przeniosło twoje własne zrzuty ekranu na ich własną stronę, by pasowały. (Jedno wydanie w tym oknie, v1.6.14, było czystą przebudową — żadnej historii, i uczciwie jest to powiedzieć.)

Trzy rzeczy, których nauczył nas strumieniowany interfejs

  1. Buforuj strumieniowane pola, publikuj raz na kawałek. Pozwolić każdemu server-sent event napędzać własną aktualizację stanu to właśnie migotanie. Akumulator w ref plus pojedyncza publikacja na kawałek, do drzewa, które nigdy się nie odmontowuje, zmienia nerwowe składanie w spokojne wypełnianie.
  2. Cokolwiek się strumieniuje, potrzebuje renderera świadomego strumieniowania. Renderer re-parsujący cały ciąg przy każdym tokenie będzie migać na niedokończonym markdown. Użyj takiego zbudowanego dla niekompletnego wejścia — a jeśli masz już jeden do innej powierzchni, użyj go ponownie, zanim zbudujesz drugi.
  3. Kopiowanie sformatowane dotyczy formatów schowka, nie serializacji. Zapisz text/html z osadzonymi obrazami oraz fallback text/plain. Schowek zaprojektowano, by nieść oba; korzystaj z tego.

To piąty rozdział opowieści o niezawodności i dopracowaniu, która staje się GeekBye v2. Po poprzedni rozdział zobacz błąd słuchawek dousznych (v1.6.12); po miejsce, gdzie ten sam pomysł server-sent events stał się później całym transportem awaryjnym, transkrypcja na żywo, gdy firewall blokuje WebSockety (v2.0.8); a po cały łuk, anatomię dostarczania oprogramowania do perfekcji.

Powiązane artykuły

Zerwane połączenie nie powinno wywalać całej aplikacji — a nasze wywalało
Steven
Steven6 min czytania

Zerwane połączenie nie powinno wywalać całej aplikacji — a nasze wywalało

Gdy nasz backend padł w trakcie spotkania, nie tylko wstrzymał transkrypcję — wywalił całą aplikację. Przyczyną było jedno nieobsłużone zdarzenie, a naprawa miała dziesięć linii. To klaster wydań, dzięki któremu GeekBye zostaje zalogowany i połączony przez rzeczy, które kiedyś go zabijały.

Inżynieria
Niezawodność
Electron
Anatomia dowożenia oprogramowania do perfekcji: jak przegląd kodu wyłapał to, czego testy nie mogły
Steven
Steven6 min czytania

Anatomia dowożenia oprogramowania do perfekcji: jak przegląd kodu wyłapał to, czego testy nie mogły

W całej serii GeekBye v2 powtarza się to samo: poprawka przechodzi każdy test na maszynie programisty, a potem przegląd kodu dowodzi, że zawiodłaby niemal u każdego. Oto workflow stojący za dziewięcioma wydaniami — bramka przeglądu, wychwyty na etapie poprawki i dyscyplina testowania-przed-wysyłką, która zamienia „u mnie działa" w „działa".

Inżynieria
Proces
Przegląd kodu
Twoja aplikacja na Maca dostaje dostęp do mikrofonu — i zapomina o nim przy każdym uruchomieniu
Steven
Steven5 min czytania

Twoja aplikacja na Maca dostaje dostęp do mikrofonu — i zapomina o nim przy każdym uruchomieniu

GeekBye poprosił o dostęp do mikrofonu, przyznałeś go, i zadziałało. Następne uruchomienie: zniknął. A aplikacja w ogóle nie pojawiła się w Ustawieniach systemowych → Mikrofon. Winowajcą była funkcja bezpieczeństwa macOS, po cichu uruchamiająca aplikację ze ścieżki, która znika — oto diagnoza i naprawa jednym kliknięciem.

macOS
Uprawnienia
Inżynieria