Чтобы документ не становился пустым после git checkout, мы смотрим на байты, а не на часы
Случалось ли вам после переключения ветки видеть пустой экран вместо открытого документа? Мы заметили такую сцену в нашем приложении, когда делали скриншот для блога, и заодно с её исправлением поймали ещё одну, более тихую ошибку.

mote безоговорочно доверяет файлу. Содержимое на экране совпадает с байтами на диске, и при сохранении записываются те же самые байты.
Но что, если файл изменится вне приложения? Если mote этого не заметит, при следующем сохранении он может перезаписать содержимое, созданное кем-то другим.
Мы обнаружили эту проблему, когда делали скриншот. Пока мы переключались между ветками, готовя экран с «изменяющимися байтами», документ иногда зависал с пустым экраном.
Две проблемы, скрывавшиеся за пустым экраном
git checkout не изменяет файл за один раз. Он удаляет существующий файл, создаёт новый, а затем записывает в него содержимое.
Наблюдатель за файлами сообщает об этом процессе несколькими событиями. Но mote начинал читать файл сразу после первого события. Иногда он считывал пустой файл, потому что содержимое ещё не успевало записаться.
При повторном чтении файла наблюдатель тоже создавался заново. За это время события, оставшиеся у предыдущего наблюдателя, пропадали, и экран так и оставался пустым.
Была и более опасная проблема.
В течение 1,5 секунды после сохранения mote считал входящие события «изменениями, сделанными мной» и игнорировал их. Но за это время форматтер или хук сохранения тоже могли снова изменить файл.
Если изменить файл через 0,3 секунды после сохранения, mote действительно никак не показывал это изменение. А когда пользователь снова сохранял документ, изменённое извне содержимое незаметно исчезало.
Одна проблема была видимой, другая — невидимой. Причина у них была одна.
Изменения файла определялись по времени, а не по содержимому.
Мы стали сравнивать содержимое вместо времени
Теперь при поступлении события файла мы не читаем его сразу. Мы ждём 200ms и начинаем отсчёт заново каждый раз, когда приходит новое событие.
После завершения процесса удаления, создания и записи файла мы только один раз считываем его последнее состояние. Устаревшие таймеры аннулируются с помощью номера поколения. Мы также не стали создавать таймер, который непрерывно работает, когда ничего не происходит.
Считанное содержимое сравнивается с последними байтами на диске, которые видел буфер.
Если они совпадают, значит, файл сохранило приложение или в него повторно записали то же содержимое. Если отличаются, значит, это настоящее внешнее изменение. Неважно, прошло после сохранения 0,1 секунды или 1 секунда.
При повторном чтении того же файла мы также сохраняем существующий наблюдатель. Так события не исчезают во время замены наблюдателя.
Мы спрашиваем только во время редактирования
Обнаружив внешнее изменение, мы проверяем состояние пользователя.
Если есть несохранённые правки, мы показываем баннер. Пользователь может выбрать, загрузить ли изменённый файл заново или продолжить редактировать текущее содержимое. При любом выборе можно потерять данные, поэтому приложение не принимает это решение за пользователя.
Если редактирование не ведётся, мы сразу загружаем новое содержимое. Баннер не появляется, а положение каретки и прокрутки сохраняется. Когда пользователь переключается между ветками и читает документ, сохранить место, на котором он остановился, естественнее, чем показывать уведомление.

Даже если файл исчез, мы не делаем выводы сразу. Возможно, другой редактор в этот момент переименовывает файл, поэтому через 300ms мы проверяем его ещё раз.
Если файла всё ещё нет, мы сообщаем об этом пользователю. Создаваемое содержимое остаётся в буфере, а при повторном сохранении файл создаётся заново.
В реальной среде мы проверяем шесть сценариев
Эту проблему трудно проверить только модульными тестами. Она возникает, когда наблюдатель за файлами, реальный диск и оконный менеджер работают вместе.
Поэтому мы создали проверки, которые выполняются в реальной среде.
Мы проверяем, следует ли буфер за файлом даже после десяти переключений ветки и отражается ли изменение файла через 300ms после сохранения. Мы также проверяем, не появляется ли уведомление, когда приложение само сохраняет файл, и показывается ли баннер при внешнем изменении во время редактирования с сохранением написанного содержимого.
Всего таких проверок шесть. Мы запускаем их при каждом выпуске.

tools/e2e/external.sh на физическом дисплее. Во всех шести строках указано ok, а в последней строке — PASS=6 FAIL=0.Считать файл источником истины — значит не только записывать сохранённое содержимое без изменений.
Если файл изменился без нашего ведома, мы не должны упустить и это изменение.