На этой неделе в преддверии праздников я был занят работой над проектом CRDT (conflict free replicated data type). Моя цель - проверить некоторые из моих предположений о создании действительно эффективного стека для автономных и локальных программ. Оставайтесь с нами, чтобы узнать больше о том, как проходило мое путешествие по реализации синхросервера для CRSQLite с помощью Go и фронтенда с использованием React + wasm SQLite. Если вы хотите узнать немного больше о том, почему я это делаю, ознакомьтесь со статьей Local First Software, опубликованной на прошлой неделе.
Основополагающей технологией, на которой я хочу построить этот проект, является CR-Sqlite. Ее можно рассматривать как слой Git поверх SQLite. Она позволяет вам создать обычную реляционную схему, затем пометить таблицы как реплицируемые, после чего вы сможете отслеживать изменения в этих таблицах с помощью простого запроса. С другой стороны синхронизации, вы можете обрабатывать слияния, вставляя изменения в базу данных, а плагин CR-Sqlite будет обновлять нужные значения в базе данных в фоновом режиме.
Синхронизация двух баз данных
Самое замечательное в CRSQLite то, что он просто работает поверх существующей базы данных. Мы начинаем с такой схемы:
CREATE TABLE foo (
id PRIMARY KEY NOT NULL,
b
);
CREATE TABLE baz (
id PRIMARY KEY NOT NULL,
b,
c,
d
);
Затем мы можем получить поток событий обновления, сделанных в базе данных, с помощью такого запроса!
result, err := conn.Query("SELECT * FROM crsql_changes WHERE db_version > ? ", currentVersion)
Специальное отношение crsql_changes содержит столбец, строку и версию базы данных для этого столбца и строки. Используя эти данные, мы можем ”материализовать” представление таблицы определенной версии. Посмотрите на реализацию, вы можете просто запросить базу данных, чтобы получить список изменений, необходимых для синхронизации двух баз данных
Чтобы объединить изменения, просто INSERT в crsql_changes и ваши строки/столбцы будут легко обновлены. Это действительно здорово, потому что это просто старый добрый SQL, и с ним очень просто взаимодействовать из Go, используя пакет database/sql. Ознакомиться с реализацией на момент написания статьи можно здесь.
Посмотрите, как все это работает вместе в ”тупой версии”, реализованной в этом тесте. Тест создает две базы данных в памяти, вставляет значения в одну из них и объединяет изменения между собой, чтобы убедиться, что обе базы данных находятся в одном и том же состоянии. После реализации этого теста я чувствую уверенность в том, что метод будет работать, и у меня есть все необходимое для внутреннего сервера, чтобы реализовать слияние состояний баз данных.
Давайте двигаться дальше.
Реализация сервера
Как мы будем передавать значения []Change по сети в осмысленном виде? Мой тест просто передает их через память, а это не совсем реалистично для нашего желаемого приложения. Мне нужно поддерживать множество веб-клиентов и простой сервер в фоновом режиме для кэширования и управления веб-клиентами. Сервер - это как клиент, только он всегда онлайн и доступен для использования в качестве источника синхронизации данных.
Мы можем решить эту проблему с помощью сервиса! Есть много способов, с которых я мог бы начать, но я решил испытать gRPC на практике. Это было ошибкой. Я надеялся на лучшее, но в итоге gRPC оказался не лучшим выбором для веб-клиента. Почему? спросите вы. Протокол gRPC работает со всеми прелестями http, когда используется от сервера к серверу, но веб-клиенты не так хороши. Javascript-клиент зависит от http 2.0, а для работы с браузером требуется прокси-сервер вроде Envoy. Более того, мне не нравилась структура созданного веб-клиента. Так что в процессе работы над этим ”локальным первым стеком” меня затянуло в большую кроличью нору, чтобы заставить работать систему rpc. В итоге я остановился на Connect, который представляет собой инструмент, позволяющий создать сервис из определения сервиса protobuf, который также работает по простому протоколу http 1.1. Что в конечном итоге помогло мне выбрать это решение как лучшее, так это то, что оно также поставляется с очень приятной в использовании генерацией веб-клиента и даже подключается к моему любимому помощнику react http useQuery.
Итак, бэкэнд готов и реализован, у меня есть упрощенное определение сервиса, реализация сервера на go и клиент, реализованный на typescript.
Реализация фронтенда
Самое крутое во всем этом стеке то, что на фронтенде у нас такая же база данных, как и на бэкенде. Это сборка всего проекта SQLite, включая плагин CR-Sqlite. Это передовой материал, давайте погрузимся в него и посмотрим, как он работает.
База данных - это действительно высокопроизводительная низкоуровневая часть системного программирования, но как, черт возьми, она работает в Интернете?
Отличный вопрос, дорогой читатель! Несмотря на то, что база данных написана на C и Rust, мы можем использовать ее из javascript, используя простой API, ориентированный на базу данных, точно так же, как мы взаимодействовали с базой данных в нашем бэкенде.
Технология, которая позволяет это сделать, - Web Assembly (WASM). Язык очень низкого уровня, который может работать в браузере и предоставлять API для обычного javascript. Это то, что поставляется в браузере, и все больше и больше становится широко используемым в Интернете. Компиляция кода на C и rust с помощью LLVM позволяет проекту CR-SQLite работать в Интернете. Я могу использовать все эти действительно продвинутые и глубоко сложные вещи через простой импорт npm. Мне нравится, как в 2023 году можно подключить тысячи строк продвинутой базы данных с помощью нескольких строк bun install :D
Завершаем
Фронтенд пока далек от совершенства, фактически есть критические ошибки в работе синхронизации, но у меня есть кости проекта - и в течение следующих недель я планирую продолжать дорабатывать и улучшать его, чтобы проверить мою теорию о том, что Local First software - это более простой, быстрый и лучший способ создания программного обеспечения. Удивительно, как много работы я могу использовать уже в пространстве CRDT, благодаря тому, как развивался веб как платформа, а также вниманию и усилиям людей по разработке отличных инструментов с открытым исходным кодом для RPC, пользовательского интерфейса и баз данных.
И последнее замечание, если вы дошли до этого момента. Спасибо, что читаете! Было бы здорово услышать от вас, так что не стесняйтесь обращаться в твиттере или в комментариях, помогите мне продвинуть этот проект (написание статей) немного дальше. Моя цель - писать раз в неделю, и я надеюсь, что этот процесс поможет мне отточить свое мастерство.