Aby dokument nie był pusty nawet po git checkout, sprawdzamy bajty zamiast czasu
Czy zdarzyło Ci się, że po zmianie gałęzi otwarty dokument nagle stał się pustym ekranem? Zobaczyliśmy tę scenę w naszej aplikacji podczas robienia zrzutów ekranu na bloga, a przy okazji naprawy wychwyciliśmy jeszcze jeden, cichszy błąd.

mote ufa plikom dokładnie takimi, jakie są. Zawartość na ekranie odpowiada bajtom na dysku, a podczas zapisywania zapisuje dokładnie te same bajty.
Co jednak, jeśli plik zmieni się poza aplikacją? Jeśli mote tego nie zauważy, przy następnym zapisie może nadpisać treść napisaną przez kogoś innego.
Odkryliśmy ten problem podczas robienia zrzutów ekranu. Gdy przełączaliśmy się między gałęziami, przygotowując ekran „zmieniających się bajtów”, dokument czasami zatrzymywał się na pustym ekranie.
Dwa problemy ukryte za pustym ekranem
git checkout nie zmienia pliku za jednym razem. Usuwa istniejący plik, tworzy nowy, a następnie zapisuje jego zawartość.
Obserwator plików zgłasza ten proces jako kilka zdarzeń. mote odczytywał jednak plik natychmiast po nadejściu pierwszego zdarzenia. Zdarzało się więc, że odczytywał pusty plik, zanim jego zawartość została zapisana.
Podczas ponownego odczytywania pliku obserwator również był tworzony od nowa. W międzyczasie znikały zdarzenia pozostałe w poprzednim obserwatorze, a ekran pozostawał pusty.
Istniał też bardziej niebezpieczny problem.
Przez 1,5 sekundy po zapisaniu mote uznawał nadchodzące zdarzenia za „zmiany wprowadzone przeze mnie” i je ignorował. W tym czasie formatter lub hook uruchamiany przy zapisie mógł jednak ponownie zmienić plik.
Gdy plik został zmieniony 0,3 sekundy po zapisie, mote rzeczywiście nie pokazywał żadnej zmiany. Jeśli użytkownik zapisał dokument ponownie, treść zmieniona poza aplikacją po cichu znikała.
Jeden problem był widoczny, drugi nie. Przyczyna była taka sama.
Zmiany pliku ocenialiśmy na podstawie czasu, a nie zawartości.
Zamiast czasu porównujemy zawartość
Teraz po nadejściu zdarzenia plik nie jest odczytywany od razu. Czekamy 200ms, a każde nowe zdarzenie uruchamia odliczanie od początku.
Po zakończeniu usuwania, tworzenia i zapisywania pliku odczytujemy tylko raz jego końcowy stan. Nieaktualne timery unieważniamy za pomocą numeru generacji. Nie tworzymy też timera, który działałby bez przerwy, gdy nic się nie dzieje.
Odczytana zawartość jest porównywana z bajtami na dysku, które bufor widział ostatnio.
Jeśli są takie same, oznacza to, że plik zapisała aplikacja albo że ponownie zapisano identyczną zawartość. Jeśli są różne, jest to rzeczywista zmiana zewnętrzna. Nie ma znaczenia, czy od zapisu minęło 0,1 sekundy, czy 1 sekunda.
Gdy ponownie odczytujemy ten sam plik, zachowujemy również istniejącego obserwatora. Dzięki temu zdarzenia nie znikają podczas jego wymiany.
Pytamy tylko podczas edycji
Po wykryciu zmiany zewnętrznej sprawdzamy stan użytkownika.
Jeśli istnieją niezapisane zmiany, pokazujemy baner. Użytkownik może wybrać, czy ponownie wczytać zmieniony plik, czy kontynuować edycję obecnej treści. W obu przypadkach istnieje ryzyko utraty treści, dlatego aplikacja nie podejmuje tej decyzji za użytkownika.
Jeśli dokument nie jest edytowany, od razu wczytujemy nową zawartość. Nie pokazujemy banera, a pozycje kursora i przewinięcia pozostają bez zmian. Podczas czytania dokumentu i przełączania się między gałęziami zachowanie miejsca, w którym użytkownik czytał, jest bardziej naturalne niż wyświetlanie powiadomienia.

Gdy plik znika, również nie podejmujemy decyzji natychmiast. Inny edytor może właśnie zmieniać jego nazwę, dlatego sprawdzamy ponownie po 300ms.
Jeśli pliku nadal nie ma, informujemy o tym użytkownika. Tworzona treść pozostaje w buforze, a ponowne zapisanie utworzy plik.
Sprawdzamy sześć rzeczy w rzeczywistym środowisku
Ten problem trudno sprawdzić wyłącznie za pomocą testów jednostkowych. Pojawia się bowiem wtedy, gdy obserwator plików, rzeczywisty dysk i menedżer okien działają razem.
Dlatego stworzyliśmy testy uruchamiane w rzeczywistym środowisku.
Sprawdzamy, czy bufor nadal podąża za plikiem po dziesięciu zmianach gałęzi oraz czy zmiana pliku 300ms po zapisie zostaje uwzględniona. Testujemy także, czy bezpośredni zapis wykonany przez aplikację nie wyświetla powiadomienia oraz czy zewnętrzna zmiana podczas edycji pokazuje baner, zachowując napisaną treść.
Łącznie jest sześć testów. Uruchamiamy je przy każdym wydaniu.

tools/e2e/external.sh na fizycznym wyświetlaczu. Wszystkie sześć wierszy ma status ok, a ostatni wiersz zawiera PASS=6 FAIL=0.Traktowanie pliku jako źródła prawdy nie kończy się na zapisywaniu treści dokładnie w takiej postaci, w jakiej została utworzona.
Gdy plik zmienia się bez naszej wiedzy, nie możemy przeoczyć również tej zmiany.