FunkcjeCennikDokumentacjaBlog
Pobierz mote
← Blog
Technika·7 września 2026·3 min czytania

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.

m
mote
Baner wyświetlany, gdy edytowany dokument zostanie zmieniony poza aplikacją. Pozwala wybrać, czy wczytać go ponownie, czy kontynuować edycję, a na pasku stanu nadal pozostaje 13 B niezapisanych danych.
Baner wyświetlany, gdy edytowany dokument zostanie zmieniony poza aplikacją. Pozwala wybrać, czy wczytać go ponownie, czy kontynuować edycję, a na pasku stanu nadal pozostaje 13 B niezapisanych danych.

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.

Plik zmieniony poza aplikacją, gdy nie był edytowany. Zawartość została zaktualizowana bez wyświetlania banera, a oznaczenie edycji w tytule i liczba oczekujących bajtów na pasku stanu zniknęły.
Plik zmieniony poza aplikacją, gdy nie był edytowany. Zawartość została zaktualizowana bez wyświetlania banera, a oznaczenie edycji w tytule i liczba oczekujących bajtów na pasku stanu zniknęły.

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.

Wynik uruchomienia tools/e2e/external.sh na fizycznym wyświetlaczu. Wszystkie sześć wierszy ma status ok, a ostatni wiersz zawiera PASS=6 FAIL=0.
Wynik uruchomienia 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.

← StarszeJak sprawiliśmy, że plik nie ulegnie uszkodzeniu, nawet jeśli program wyłączy się podczas zapisywania
Bądź na bieżąco przez RSS.
Nic poza pisaniem.
Polski
© 2026 mote