Меню Закрыть

Модель данных, лежащая в основе гибкости Ноушен

Последнее изменение: 20.10.2023
Вы здесь:
Расчетное время чтения: 8 мин

Поколение первопроходцев (Дуг Энгельбарт, Тед Нельсон, Алан Кей и многие другие) рассматривало компьютер как инструмент, дополняющий решение проблем человеком, предоставляя ему власть над информацией.

Сегодня эта информация в большинстве случаев остается разрозненной в разных инструментах. Возьмем, к примеру, облачные редакторы документов, в которых страницы являются наименьшей атомарной единицей. Информация заперта внутри страниц, файлов и папок — это напоминает то, как все делалось сто лет назад.

Мы построили Ноушен на основе фреймворка, который позволяет информации быть самостоятельной, свободной от каких-либо ограничений или контейнеров, вместо этого передавая власть в руки пользователя на гранулярном уровне. Эта основа построена на блоках.

Все, что вы видите в Ноушен, является блоком. Текст, изображения, списки, строки в базе данных, даже сами страницы — все это блоки, динамические единицы информации, которые могут быть преобразованы в другие типы блоков или свободно перемещаться внутри Ноушен. Это LEGO, из которых мы строим и моделируем информацию. А собранные вместе, блоки подобны наборам LEGO, создавая нечто гораздо большее, чем сумма их частей.

Такая гибкость лежит в основе миссии Ноушен. В то время как блоки требуют от нашей инженерной команды строгой структуризации информации, мы хотели создать атомарную, графоподобную модель данных, чтобы предоставить нашим пользователям возможность настраивать способы перемещения, организации и обмена информацией.

Блокчейн-модель делает Ноушен уникальной, и именно она является основой того, как Ноушен думает о воплощении в жизнь того, чем, по мнению первопроходцев, может стать компьютер как среда.

Основы блокчейна

Подобно блокам из набора LEGO, блоки Ноушен — это отдельные элементы, представляющие все единицы информации в редакторе Ноушен. Атрибуты блока определяют, как эта информация отображается и организуется.

Модель данных, лежащая в основе гибкости Ноушен

Каждый блок имеет следующие атрибуты:

  • ID — каждый блок однозначно идентифицируется по своему ID. ID блоков страницы можно увидеть в конце URL-адреса в браузере. Для идентификаторов в Ноушен мы используем случайно генерируемые UUID (UUID v4).
  • Свойства — структура данных, содержащая пользовательские атрибуты конкретного блока. Наиболее распространенным свойством является title, в котором хранится текстовое содержимое таких типов блоков, как абзацы, списки и, конечно, заголовок страницы. Более сложные типы блоков требуют дополнительных или иных свойств, например, блок страницы в базе данных с пользовательскими свойствами.
  • Тип — каждый блок имеет тип, который определяет способ отображения блока и интерпретацию его свойств. Ноушен поддерживает множество типов блоков, большинство из которых можно увидеть в меню «новый блок», появляющемся при нажатии кнопки +, или в меню /:
Using Ноушен’s slash menu, you can add different types of blocks to Ноушен pages.

Помимо атрибутов, описывающих сам блок, каждый блок имеет атрибуты, определяющие его взаимоотношения с другими блоками:

  • Content — массив (или упорядоченный набор) идентификаторов блоков, представляющих содержимое внутри данного блока, например, вложенные элементы в маркированном списке или текст внутри тумблера.
  • Parent — идентификатор родительского блока. Родительский блок используется только для получения разрешений.

Как блоки соединяются между собой

Подобно блокам LEGO, блоки Ноушен можно комбинировать с другими блоками, создавая нечто гораздо более мощное — например, дорожную карту, полностью адаптированную к процессу работы вашей команды, отслеживающую прогресс и хранящую всю информацию о проекте в одном месте. Мы организуем все аспекты блоков таким образом, чтобы они выполняли нужные действия и находились в нужных местах, позволяя пользователям соединять их и адаптировать Ноушен для решения своих задач.

Тип и свойства

Тип блока определяет, как он будет отображаться в пользовательском интерфейсе Ноушен, и в зависимости от этого типа мы по-разному интерпретируем свойства и содержимое блока. Вы можете быть знакомы с этим, если использовали функцию Turn into в Ноушен, которая позволяет превратить один тип блока в другой.

Изменение типа блока не приводит к изменению его свойств или содержимого — меняется только атрибут type. Информация просто отображается по-другому или даже игнорируется, если свойство не используется в данном типе блока.

Например, здесь видно, что блок To-do list преобразуется в несколько других типов блоков. Мы также проверяем этот элемент списка дел. Свойство «checked» блока To-do list игнорируется при преобразовании блока в блоки типа Heading и Callout, но когда мы проходим полный круг и снова превращаем блок в блок To-do list, он все еще остается отмеченным.

Отделение хранения свойств от типа блока позволяет эффективно преобразовывать и изменять нашу логику рендеринга. Но это также необходимо для совместной работы, поскольку мы сохраняем как можно больше пользовательских намерений.

Контент и дерево рендеринга

Гибкость блочной модели также позволяет вставлять блоки внутрь других блоков, например, текст внутрь тумблера или бесконечно вложенные подстраницы внутри страниц. В атрибуте content блока хранится массив идентификаторов блоков (или указателей), ссылающихся на вложенные блоки.

Модель данных, лежащая в основе гибкости Ноушен

В примере со списком дел у нас есть блок To-do list («Write a blog post about blocks») с тремя идентификаторами блоков в массиве content. Мы считаем эти идентификаторы «нисходящими указателями», а блоки, на которые они ссылаются, называем «содержимым» или «дочерними блоками рендеринга».

Модель данных, лежащая в основе гибкости Ноушен

Каждый блок определяет позицию и порядок, в котором отображаются его блоки содержимого. Мы называем эту иерархическую связь между блоками и их дочерними элементами «деревом рендеринга». Но оно не похоже на дерево с ветвями — разные типы блоков визуализируют свои дочерние блоки по-разному.

Приведем несколько примеров того, как атрибут content отображается в различных типах блоков:

  • Блоки списка — Текстмаркированный список и список дел. Блоки списка отображают свое содержимое с отступом.
  • Toggles — блоки списка Toggle отображают содержимое только в развернутом виде. В противном случае они отображают только свойство title.
  • Страницы — Блоки страниц отображают свое содержимое на новой странице, вместо того чтобы выводить его с отступом на текущей странице. Чтобы увидеть это содержимое, необходимо щелкнуть мышью на новой странице.

Структурно это продолжает привносить мощь в вашу информацию, позволяя манипулировать блоками на самом детальном уровне. Это концепция, которая сохраняет пользовательское представление о том, как должна быть организована и отображена информация при работе с другой информацией.

Редактирование дерева рендеринга

Удивлялись ли вы когда-нибудь тому, как работает отступ в Ноушен? В обычных текстовых процессорах отступы носят презентационный характер: они влияют только на расстояние между текстом и полями. В Ноушен отступы являются структурными: они отражают структуру дерева рендеринга. Другими словами, когда вы делаете отступ в Ноушен, вы управляете отношениями между блоками и их содержимым, а не просто добавляете стиль.

Например, при нажатии кнопки «Отступ» в блоке содержимого происходит попытка добавить этот блок к содержимому ближайшего блока-родственника в дереве содержимого.

Модель данных, лежащая в основе гибкости Ноушен

В большинстве случаев отступы работают так же, как и в традиционном редакторе документов — выбранный блок перемещается в массив содержимого предыдущего блока и отображается с отступом. Однако если предыдущий блок не является списком (или предыдущий блок вообще отсутствует), то отступы не будут иметь никакого эффекта, поскольку блоку некуда перемещаться. Визуальное представление документа в Ноушен отражает структуру содержащейся в нем информации.

Разрешения

До сих пор мы объясняли, как блоки объединяются для организации и структурирования информации. Важно также понимать, как эта структура защищает информацию, чтобы только нужные люди могли ее прочитать или изменить.

Блоки наследуют права доступа в зависимости от блоков, в которых они расположены (которые находятся над ними в дереве). Рассмотрим страницу — чтобы прочитать ее содержимое, необходимо иметь возможность читать блоки, находящиеся на этой странице. Однако есть две причины, по которым мы не можем использовать массив содержимого для построения такой системы прав доступа:

  1. Изначально мы разрешили блокам ссылаться на несколько массивов содержимого, чтобы упростить модель совместной работы и параллелизма. Но поскольку на блок можно ссылаться в нескольких местах, становится неясным, от какого блока он наследует права. А двусмысленность в системе разрешений недопустима.
  2. Вторая причина — механическая. Чтобы реализовать проверку прав для блока, необходимо просмотреть дерево, получив предков этого блока вплоть до корня дерева (которым является рабочая область). Пытаться найти этот путь предков, перебирая массивы содержимого всех блоков, неэффективно, особенно на клиенте.
Модель данных, лежащая в основе гибкости Ноушен

Вместо этого мы используем «восходящий указатель» — атрибут parent — для системы разрешений. Восходящий родительский указатель и нисходящий указатель содержимого зеркально отражают друг друга (за исключением нескольких крайних случаев, над устранением которых мы сейчас работаем).

Модель данных, лежащая в основе гибкости Ноушен

Жизнь блока

Жизнь блока начинается с клиента.

Когда вы выполняете какое-либо действие в пользовательском интерфейсе — набираете текст в редакторе, перетаскиваете блоки по странице, — эти изменения выражаются в виде операций, создающих или обновляющих одну запись. Под «записями» наша команда понимает любой вид сохраняемых данных в Ноушен, например, блоки, пользователей, рабочие пространства и т.д. Поскольку многие действия обычно изменяют более одной записи, операции объединяются в транзакции, которые фиксируются (или отклоняются) сервером как группа.

Допустим, вы работаете на одной странице с другом, оба на разных компьютерах, редактируя список дел. Что происходит за кулисами?

Создание и обновление блоков

Вы нажимаете клавишу Enter — создается новый блок » Дела".

Сначала клиент определяет все начальные атрибуты блока, генерируя новый уникальный идентификатор, устанавливая соответствующий тип блока(to_do) и заполняя свойства блока (пустой заголовок и checked: [["No"]]). На основе этого строятся операции, представляющие создание нового блока с этими атрибутами.

Новые блоки создаются не сами по себе: блоки также добавляются в массив содержимого их родителя, чтобы они занимали правильное положение в дереве содержимого. Поэтому клиент также генерирует операцию для этого. Все эти отдельные операции изменения объединяются в транзакцию.

Затем клиент применяет операции транзакции к своему локальному состоянию. При этом в памяти создаются новые блочные объекты и модифицируются существующие блоки. В наших собственных приложениях все записи, к которым вы обращаетесь локально, мы кэшируем в LRU (least recently used) кэше поверх SQLite или IndexedDB под названием RecordCache. Когда вы изменяете записи в родном приложении, мы также обновляем локальные копии в RecordCache. Редактор выполняет повторный рендеринг, чтобы отрисовать на экране вновь созданный блок. Это происходит в течение нескольких миллисекунд после нажатия клавиши.

В то же время транзакция сохраняется в TransactionQueue — части клиента, отвечающей за отправку всех транзакций на серверы Ноушен, чтобы ваши данные сохранялись и были доступны для совместной работы. TransactionQueue надежно хранит транзакции в IndexedDB или SQLite (в зависимости от платформы) до тех пор, пока они не будут персистированы сервером или отклонены.

Сохранение изменений на сервере

Вот как ваш блок будет сохранен на сервере, чтобы ваш друг мог его увидеть.

Обычно TransactionQueue пуста, поэтому транзакция по созданию блока сразу отправляется на сервер Ноушен в API-запросе. Данные транзакции сериализуются в JSON и публикуются в конечной точке API /saveTransactions.

Основная задача SaveTransaction — доставить ваши данные в наши базы данных-источники, в которых хранятся все блочные данные, а также все другие виды персистируемых записей в Ноушен.

После того как запрос достигнет сервера Ноушен API:

  1. Мы загружаем все блоки и родителей, участвующих в транзакции. Это дает нам картину «до» в памяти. Для данного примера следует помнить, что мы создаем блок. Поэтому нам необходимо, как минимум, загрузить блок страницы, чтобы мы могли вставить идентификатор вновь созданного блока в массив содержимого страницы.
  2. Мы дублируем данные «до», которые только что были загружены в память. Затем мы применяем операции транзакции к новой копии, чтобы создать данные «после».
  3. Затем мы используем данные «до» и «после» для проверки изменений на соответствие разрешениям и целостности данных. Если все подтвердилось (а обычно так и бывает), все созданные или измененные записи фиксируются в базе данных, что означает, что ваш блок уже официально создан.
  4. В этот момент на исходный API-запрос, отправленный клиентом, приходит HTTP-ответ «success». Это подтверждает, что транзакция была успешно сохранена, и клиент может перейти к сохранению следующей транзакции в очереди транзакций.
  5. В фоновом режиме мы планируем дополнительные работы в зависимости от типа изменений, внесенных в вашу транзакцию. Например, мы планируем снимки истории версий и индексирование текста блока для быстрого поиска. Важно отметить, что мы также уведомляем MessageStore — службу обновлений Ноушен в режиме реального времени — о внесенных вами изменениях.

О том, как данные попадают на экран вашего друга, мы расскажем в следующем разделе.

Обновления в режиме реального времени

Вы нажали клавишу Enter, создали новый блок, и теперь ваш блок отображается на экране вашего друга. Как это работает?

Каждый клиент имеет долговременное WebSocket-соединение с MessageStore, службой обновлений Ноушен в реальном времени. Когда клиент Ноушен отображает блок (или страницу, или любой другой вид записи), он подписывается на изменения этой записи от MessageStore, используя это WebSocket-соединение. Когда ваш друг открывает ту же страницу, что и вы, он подписывается на изменения всех этих блоков.

После того как ваши изменения прошли через процесс saveTransactions, API уведомил MessageStore о новых версиях записей. MessageStore находит клиентские соединения, подписанные на эти изменившиеся записи, и передает им новую версию через их WebSocket-соединение.

Когда клиент вашего друга получает уведомления об обновлении версии от MessageStore, он проверяет версию блока в своем локальном кэше. Поскольку версии из уведомления и локального блока отличаются, он посылает API-запрос syncRecordValues на сервер со списком устаревших записей клиента. Сервер отвечает новыми данными о записях. Клиент использует эти ответные данные для обновления локального кэша новой версией записей, а затем перестраивает пользовательский интерфейс для отображения последних данных блока.

Блоки для чтения

Ваш друг ложится спать, а вы продолжаете работать над списком дел. Чтобы сообщить ему, что вы внесли в список некоторые изменения, вы посылаете ему ссылку на страницу Ноушен, на которой вы оба работали.

В первые несколько миллисекунд после того, как ваш друг проснулся и щелкнул на ссылке, мы сначала пытаемся загрузить эту страницу, используя только локальные данные. В веб-приложениях это означает блоки данных, находящиеся в памяти. В нативных приложениях мы пытаемся загрузить блоки, не находящиеся в памяти, из хранилища RecordCache. Но если нам нужны данные блока, который отсутствует, мы останавливаемся и запрашиваем данные страницы из API.

Метод API для загрузки данных для страницы называется loadPageChunk — он спускается от начальной точки (скорее всего, идентификатора блока страницы) вниз по дереву контента и возвращает блоки в дереве контента плюс все зависимые записи, необходимые для правильного отображения этих блоков. Мы используем несколько уровней кэширования для loadPageChunk, но в худшем случае этому API может потребоваться много обращений к базе данных, поскольку он рекурсивно ползет вниз по дереву, чтобы найти блоки и их зависимости от записей.

Все данные, загруженные loadPageChunk, помещаются в память (и сохраняются в RecordCache, если вы используете приложение). После того как данные окажутся в памяти, мы создадим страницу и отрисуем ее с помощью React.

Строительные блоки для того, что будет дальше

Блоки являются основополагающим компонентом миссии Ноушен, позволяющей любому человеку или предприятию адаптировать программное обеспечение к своим проблемам. Эта архитектура задает направление развития Ноушен — новые типы блоков, автоматизация(например, API), рабочие процессы и функциональность, позволяющая создавать еще более мощные инструменты.

Тем не менее, мы знаем, что есть куда расти продукту Ноушен, улучшая такие вещи, как эффективность, производительность, автономность и специфические особенности редактора. То, что вы видите при работе с Ноушен, — это лишь часть айсберга, которая находится над поверхностью. Мы надеемся, что после прочтения этой статьи вы поймете, что находится под ней.

Нам нужна помощь в создании будущего Ноушен. Это вы?

Была ли эта статья полезной?
Нет 0
Просмотров: 21

Читать далее

Предыдущий: Наша политика в отношении содержания и использования материалов
Следующий: Учебник для стартапов по организации межфункционального взаимодействия