Перейти к основному содержимому

Удаленное хранение

Отправлять все состояние редактора при каждом изменении неэффективно.
Для таких случаев редактор предоставляет API для репликации данных в реальном времени.

Установка​

npm install @dineug/erd-editor

Хранилище репликации находится в подпути @dineug/erd-editor/engine.js, а не в корне пакета.
Эта точка входа не обращается к DOM, поэтому работает в Web Worker или в любом другом месте вне основного потока.
Если же импортировать корень пакета, регистрируется пользовательский элемент <erd-editor>, которому нужен документ.

Использование​

type ReplicationStore = {
readonly value: string;
readonly runtimeValue: string;
on: (
listeners: Partial<{ change: (change: ReplicationChange) => void }>
) => Unsubscribe;
setInitialValue: (value: string) => void;
dispatch: (actions: Array<AnyAction> | AnyAction) => void;
dispatchSync: (actions: Array<AnyAction> | AnyAction) => void;
destroy: () => void;
};

type ReplicationChange = {
value: string;
changed: boolean;
};

type InjectEngineContext = {
toWidth: (text: string) => number;
};

type CreateReplicationStore = (
context: InjectEngineContext
) => ReplicationStore;

// example
import { createReplicationStore } from '@dineug/erd-editor/engine.js';

const replicationStore = createReplicationStore({
toWidth: text => text.length * 10,
});

Репликация работающего редактора​

Действия поступают из общего хранилища работающего редактора.
См. Совместное редактирование.

import { createReplicationStore } from '@dineug/erd-editor/engine.js';

const replicationStore = createReplicationStore({ toWidth });
replicationStore.setInitialValue(editor.runtimeValue);

replicationStore.on({
change: ({ value, changed }) => {
if (changed) save(value);
},
});

const sharedStore = editor.getSharedStore();
sharedStore.subscribe(actions => {
replicationStore.dispatch(actions);
});

Через границу передается только изменение и никогда — весь документ.
Реплика применяет его и сериализует результат на своей стороне.
Событие change передает этот результат в свойстве value, а свойство changed показывает, когда его стоит записывать.

Начиная с версии 4.0.0 инициализируйте реплику из свойства runtimeValue редактора, как показано выше, а не из сохраненного файла или свойства value редактора.
В файле нет правок, сделанных после его сохранения, а в свойстве value нет сущностей, удаленных в ходе сеанса.
Если реплика инициализирована без удаленной таблицы, то, когда редактор отменяет удаление, она возвращает таблицу без имени и столбцов и в таком виде ее сохраняет.

InjectEngineContext​

toWidth​

Измеряет ширину строки в пикселях.
У хранилища нет DOM, поэтому само измерять текст оно не может и пересчитывает ширину столбцов с помощью этой функции всякий раз, когда меняется имя, тип данных, значение по умолчанию или комментарий.
Кроме того, оно заново измеряет все таблицы и столбцы всякий раз, когда метод setInitialValue загружает документ.
Начиная с версии 4.0.0 значения ширины хранятся только в свойстве runtimeValue и никогда — в свойстве value.
Поэтому документ, сохраненный этой версией, при повторном чтении дает побайтно то же значение value, где бы он ни был сохранен и как бы там ни измерялся текст.

Измеряйте текст так же, как редактор, — 400 12px в наборе шрифтов редактора, с округлением и добавлением отступа 2 px.
Иначе значения ширины в свойстве runtimeValue будут расходиться с теми, что на экране.

const TEXT_PADDING = 2;
const FONT =
"400 12px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, " +
"'Helvetica Neue', 'Open Sans', system-ui, sans-serif, " +
"'Apple Color Emoji', 'Segoe UI Emoji'";

let context = null;

function getContext() {
if (context) return context;

try {
context = new OffscreenCanvas(0, 0).getContext('2d');
if (context) context.font = FONT;
} catch {
// no canvas available
}

return context;
}

function toWidth(text) {
const context = getContext();
const width = context ? context.measureText(text).width : text.length * 10;

return Math.round(width) + TEXT_PADDING;
}

const replicationStore = createReplicationStore({ toWidth });

text.length * 10 — запасной вариант на случай, когда холст недоступен.
Его достаточно для хранилища, документ которого только сохраняется и никогда не отображается.

ReplicationStore​

value getter​

Сериализует текущее состояние редактора.
Возвращает строку JSON, которую можно сохранить как есть, — а не разобранный объект.

const data = replicationStore.value;

Начиная с версии 4.0.0 свойство value — это форма хранения: документ в том виде, в каком он хранится в файле, тот же текст, который возвращает свойство value элемента.
Оно содержит только действующие сущности в фиксированном порядке, без удаленных сущностей, порядка наложения и всего, что заново вычисляется при загрузке, например значений ширины, которые измеряет функция toWidth.
Оно записывается с отступом в два пробела и заканчивается переводом строки, поэтому один и тот же документ всегда дает одни и те же байты.
О том, что оно содержит, см. Форма хранения и значение времени выполнения.

Каждая настройка, блокировка которой включена, записывается с заблокированным значением, а все остальные настройки — с текущим.
Блокировка Viewport охватывает сразу начало координат вида и масштаб, а для вкладки, языка Генератора кода, обоих стилей имен и типа кавычек предусмотрено по отдельной блокировке.
Блокировки заменяют поле ignoreSaveSettings документа, которое начиная с версии 4.0.0 не читается и не записывается.
До версии 4.0.0 свойство value содержало весь документ в том виде, в каком он находился в хранилище, с примененными битами прокрутки и масштаба из этого поля.

runtimeValue getter​

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

const snapshot = replicationStore.runtimeValue;

Используйте это значение, чтобы инициализировать редактор или другое хранилище, которое подключается позже, через его метод setInitialValue; см. Совместное редактирование.
Никогда не сохраняйте это значение как файл: оно содержит удаленные сущности, а опубликованная JSON Schema его отклоняет.
Именно эти удаленные сущности позволяют отмене удаления, отправленной позже, восстановить сущность целиком в хранилище, инициализированном из этого значения.
Они сохраняются до следующего открытия документа из файла, форма которого их не содержит.

setInitialValue​

Загружает ранее сохраненное состояние редактора.

replicationStore.setInitialValue('json...');

Метод принимает форму хранения, значение времени выполнения или документ, сохраненный более ранней версией, включая документ v2.
Пустая строка или любое значение, не являющееся строкой, загружает новый пустой документ, а не вызывает ошибку.
Хранилище, которому ни разу не передавалось значение, начинает работу с такого же нового документа.
Начиная с версии 4.0.0 в этом документе включены все блокировки, поэтому его свойство value сохраняет исходные вид, вкладку, язык Генератора кода, стили имен и тип кавычек, пока реплицируемые редакторы прокручивают холст, меняют масштаб, переключают вкладки или меняют эти настройки. В версии 3.10.0 в нем не сохранялись ни прокрутка, ни масштаб, а до этого сохранялось и то и другое.
Для сохраненного документа действуют указанные в нем блокировки.
Документ, в котором они не указаны, а это любой документ, сохраненный до версии 4.0.0, включая документ v2, загружается со всеми включенными блокировками: начало координат вида фиксируется на 0, 0, масштаб — на 1, вкладка — на ERD, что бы в нем ни было сохранено, а язык Генератора кода, стили имен и тип кавычек — на сохраненных значениях.
Начиная с версии 4.0.0 метод setInitialValue также сбрасывает регистры совместной работы, хранившиеся для предыдущего документа, как и метод setInitialValue элемента.
Текст, не являющийся JSON, например файл с оставшимися маркерами конфликта слияния, тоже не вызывает ошибку: ошибка разбора только выводится в консоль, документ очищается, а настройки остаются прежними.

Начиная с версии 4.0.0 при загрузке не выполняется сборка мусора, поэтому удаленные сущности остаются в свойстве runtimeValue.
Раньше при загрузке удалялись сущности, которые больше не встречались в поле doc и не изменялись четыре дня или дольше.

Перезаписи при загрузке завершаются до того, как метод setInitialValue вернет управление: метки ключей у столбцов, считываемые из параметров каждого столбца и из связей; ширина каждой таблицы и каждого столбца, заново измеренная функцией toWidth; точки, в которых каждая линия связи соединяется со своими таблицами; а также признак идентифицирующей связи и символ на родительском конце у каждой связи.
Ничто из этого не входит в свойство value, но значение replicationStore.runtimeValue, прочитанное сразу после вызова, уже содержит все это.
Действие, поступившее сразу после загрузки, например повторно воспроизведенная прокрутка, сравнивается с загруженным документом, поэтому сама загрузка никогда не отмечается как changed.

Ни загрузка, ни какая-либо из этих перезаписей не генерирует событие change.
Если нужно записать загруженный документ обратно в форме хранения, прочитайте replicationStore.value самостоятельно после того, как метод setInitialValue вернет управление.

on@change​

Подписывается на изменения состояния редактора.

const unsubscribe = replicationStore.on({
change: ({ value, changed }) => {
if (changed) {
// save value...
}
},
});

Событие change генерируется с задержкой (debounce) 200 мс, поэтому серия действий приходит одним уведомлением.
Оно возникает только для действий тех типов, которые могут изменить документ, а также для действия settings.changeIgnoreSaveSettings, которое еще может отправлять более старый редактор (см. dispatch, dispatchSync).
Ретранслированное действие со сведениями о присутствии или действие editor.mergeLWW никогда его не вызывают.
Чтобы отписаться, вызовите возвращенную функцию.

Начиная с версии 3.10.0 обработчик получает значение типа ReplicationChange, который точка входа @dineug/erd-editor/engine.js экспортирует наряду с ReplicationStore.
До версии 3.10.0 он не получал ничего, а обработчик, игнорирующий аргумент, продолжает работать.
Свойство value — это сериализованный документ в форме хранения, та же строка, которую в этот момент возвращает replicationStore.value, поэтому повторно сериализовать его не нужно.
Свойство changed равно false, если действия, выполненные после предыдущего события change или после загрузки, оставили value побайтно неизменным.

Изменение, которое блокировка исключает из value, все равно вызывает событие change со свойством changed, равным false, поэтому записывайте документ, только когда changed равно true.
Это прокрутка или изменение масштаба, пока действует блокировка Viewport, переключение вкладки, пока действует блокировка Canvas Type, а также изменение языка Генератора кода, стиля имен или типа кавычек, пока включена соответствующая блокировка.
Событие change элемента не учитывает такое изменение, но реплика все равно о нем сообщает.
В новом документе и в документе, сохраненном до версии 4.0.0, включены все блокировки.
Если блокировка Viewport снята, прокрутка или изменение масштаба меняют value, а если снята блокировка Canvas Type, value меняет и открытие другой вкладки редактора.
Включение или выключение блокировки также меняет value.

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

При вычислении changed сравнение никогда не выполняется с сохраненным файлом, который уже после загрузки может отличаться от value: так происходит с файлом, сохраненным более ранней версией, включая файл v2, а также с файлом, который начинается с метки порядка байтов, и с файлом, отредактированным вручную.
При побайтном сравнении с таким файлом правкой считалась бы даже прокрутка, которую исключает блокировка.
Первое событие change со свойством changed, равным true, возвращает такой файл в форме хранения, поэтому при записи файл будет перезаписан один раз.
Файл, сохраненный этой версией, не отличается, где бы он ни был сохранен, как объясняется в разделе toWidth.
Если вызов setInitialValue происходит, пока событие change еще ожидает отправки, и до наступления его срока ничего не отправляется, это событие change все равно приходит с загруженным value и со свойством changed, равным false.

dispatch, dispatchSync​

Применяет изменения удаленного редактора к хранилищу replicationStore.

replicationStore.dispatch(actions); // async
replicationStore.dispatchSync(actions); // sync

Метод dispatch откладывает применение до микрозадачи, а dispatchSync применяет пакет сразу.

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

Начиная с версии 3.10.0 в число принимаемых действий входят relationship.changeOnDelete и relationship.changeOnUpdate, а действие relationship.add передает поля onDelete и onUpdate.
Хранилище более ранней версии отбрасывает эти действия и игнорирует эти поля, поэтому теряет у связей действия ON DELETE и ON UPDATE.

Начиная с версии 4.0.0 в число принимаемых действий также входят table.changeGroup, семь из восьми действий tableGroup (tableGroup.add, tableGroup.move, tableGroup.moveTo, tableGroup.remove, tableGroup.resize, tableGroup.changeName и tableGroup.changeColor, все, кроме изменения порядка наложения tableGroup.changeZIndex), relationship.changeColumns, settings.changeLockSettings и settings.changeDDLScript.
Действия settings.changeRelationshipOptimization больше не существует, поэтому оба метода его отбрасывают.
Хранилище более ранней версии отбрасывает новые действия, поэтому теряет группы таблиц, блокировки, скрипты Схемы SQL и столбцы, которые окно «Сопоставить столбцы» задает для существующей связи.
Запускайте реплику на той же версии, что и редакторы, или на более новой.

Версия 4.1.0 не добавляет ни одного типа действий и оставляет @dineug/erd-editor/engine.js без изменений, поэтому реплика версии 4.0.0 принимает все изменения, которые отправляет редактор версии 4.1.0, точно так же, как реплика версии 4.1.0.
Начиная с версии 4.1.0 кнопки Replace и Replace All в Поиске и замене также переименовывают индексы и группы таблиц с помощью действий index.changeName и tableGroup.changeName, которые реплика версии 4.0.0 уже принимает.
Собственное состояние Обозревателя никогда не отправляется, а переключение вкладки или прокрутка, выполненные из него, приходят так же, как любые другие переключения вкладок и прокрутки; см. Совместное редактирование.

Начиная с версии 4.0.0 оба метода также применяют действие editor.mergeLWW, которое передает регистры совместной работы, отправляемые редактором в ответ на подключение к сеансу другого редактора, поэтому реплика согласует одновременные правки так же, как редакторы.
Оно не меняет ни одного байта value и не вызывает событие change.
Действие settings.changeIgnoreSaveSettings, которое еще может отправлять редактор, выпущенный до версии 4.0.0, принимается, но ничего не меняет: оно вызывает событие change со свойством changed, равным false.

destroy​

Полностью уничтожает экземпляр replicationStore.

replicationStore.destroy();

После этого хранилище перестает генерировать событие change и игнорирует дальнейшие отправки действий.
Документ застывает на своем последнем значении, не вызывая ошибку.
Чтобы начать заново, создайте новое хранилище.