Удаленное хранение
Отправлять все состояние редактора при каждом изменении неэффективно.
Для таких случаев редактор предоставляет API для репликации данных в реальном времени.
Установка
npm install @dineug/erd-editor
Хранилище репликации находится в подпути @dineug/erd-editor/engine.js, а не в корне пакета.
Эта точка входа не обращается к DOM, поэтому работает в Web Worker или в любом другом месте вне основного потока.
Если же импортировать корень пакета, ему нужен документ: начиная с версии 5.0.0 он не определяет ни одного элемента, но при вычислении создает оба класса элементов. До версии 5.0.0 он регистрировал пользовательский элемент <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 нет сущностей, удаленных в ходе сеанса.
Если реплика инициализирована без удаленной таблицы, то, когда редактор отменяет удаление, она возвращает таблицу без имени и столбцов и в таком виде ее сохраняет.
Начиная с версии 4.2.0 то же относится к слову, термину или домену, удаленному с листа Словаря.
Когда редактор отменяет удаление, реплика, инициализированная без этой строки, снова выводит ее пустой, а так как value не включает пустую строку, реплика сохраняет документ без нее.
Если это домен, то столбцы и термины, которые были к нему привязаны, в этой реплике также остаются не привязанными, так как привязок в ней никогда не было, а отмена их не передает.
Начиная с версии 5.1.0 пакет @dineug/erd-editor-yjs также может синхронизировать редакторы через комнату Yjs.
Реплика, получающая данные описанным выше способом от элемента, привязанного к комнате, получает только правки, сделанные на этом элементе, но не те, что приносит комната: они доходят до элемента как правки другого редактора, а общее хранилище никогда не пересылает дальше то, что получило.
Чтобы сохранить документ комнаты, сохраните value привязанного элемента или привязанного однорангового хранилища, как описано в разделе Что хранит комната.
Провайдер постоянного хранения Yjs сохраняет саму комнату, которая содержит значение времени выполнения и сделанные после него правки, а не файл; см. Ограничения.
InjectEngineContext
toWidth
Измеряет ширину строки в пикселях.
У хранилища нет DOM, поэтому само измерять текст оно не может и пересчитывает ширину столбцов с помощью этой функции всякий раз, когда меняется имя, тип данных, значение по умолчанию или комментарий.
Начиная с версии 4.2.0 сюда входит и логическое имя таблицы или столбца.
Ячейку домена столбца, в которой отображается имя домена, к которому привязан столбец, оно измеряет всякий раз, когда столбец привязывается к домену или отвязывается от него, а также когда этот домен добавляется, переименовывается или удаляется.
Кроме того, оно заново измеряет все таблицы и столбцы всякий раз, когда метод 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.
Начиная с версии 4.2.0 оно также содержит логические имена, признак того, сформировано ли каждое имя по словарям, привязки к доменам, а также слова, термины и домены вкладки «Словарь».
Кроме того, начиная с версии 4.2.0 оно не включает пустые текстовые поля сущностей, списки doc и коллекции, в которых ничего нет, а также само поле collections, если все коллекции пусты.
Оно записывается с отступом в два пробела, каждая строка словаря — на отдельной строке, и заканчивается переводом строки, поэтому один и тот же документ всегда дает одни и те же байты.
О том, что оно содержит, см. Форма хранения.
Каждая настройка, блокировка которой включена, записывается с заблокированным значением, а все остальные настройки — с текущим.
Блокировка Viewport охватывает сразу начало координат вида и масштаб, а для вкладки, языка Генератора кода, обоих стилей имен и типа кавычек предусмотрено по отдельной блокировке.
Начиная с версии 4.2.0 отдельная блокировка есть и у режима отображения имен: блокировка Name View включена в новом документе.
Блокировки заменяют поле ignoreSaveSettings документа, которое начиная с версии 4.0.0 не читается и не записывается.
До версии 4.0.0 свойство value содержало весь документ в том виде, в каком он находился в хранилище, с примененными битами прокрутки и масштаба из этого поля.
runtimeValue getter
Начиная с версии 4.0.0 сериализует документ в том виде, в каком его хранит реплика, включая удаленные сущности и все, что вычисляется при загрузке.
Каждая заблокированная настройка и здесь записывается со своим заблокированным значением.
const snapshot = replicationStore.runtimeValue;
Используйте это значение, чтобы инициализировать редактор или другое хранилище, которое подключается позже, через его метод setInitialValue; см. Совместное редактирование.
У реплики нет getLWW и getVersion, поэтому вместе с runtimeValue она не передает регистров совместной работы: редактор, инициализированный из нее, получает их от работающих редакторов, как описано в разделе Инициализация нового участника.
Никогда не сохраняйте это значение как файл: оно содержит удаленные сущности, а опубликованная JSON Schema его отклоняет.
Именно эти удаленные сущности позволяют отмене удаления, отправленной позже, восстановить сущность целиком в хранилище, инициализированном из этого значения.
Они сохраняются до следующего открытия документа из файла, форма которого их не содержит.
setInitialValue
Загружает ранее сохраненное состояние редактора.
replicationStore.setInitialValue('json...');
Метод принимает форму хранения, значение времени выполнения или документ, сохраненный более ранней версией, включая документ v2.
Пустая строка или любое значение, не являющееся строкой, загружает новый пустой документ, а не вызывает ошибку.
Хранилище, которому ни разу не передавалось значение, начинает работу с такого же нового документа.
Начиная с версии 4.0.0 в этом документе включены все блокировки, поэтому его свойство value сохраняет исходные вид, вкладку, язык Генератора кода, стили имен и тип кавычек, пока реплицируемые редакторы прокручивают холст, меняют масштаб, переключают вкладки или меняют эти настройки. В версии 3.10.0 в нем не сохранялись ни прокрутка, ни масштаб, а до этого сохранялось и то и другое.
Начиная с версии 4.2.0 он также сохраняет свой режим отображения имен, Physical Names, пока редакторы переключают свои.
Для сохраненного документа действуют указанные в нем блокировки.
Документ, в котором они не указаны, а это любой документ, сохраненный до версии 4.0.0, включая документ v2, загружается со всеми включенными блокировками: начало координат вида фиксируется на 0, 0, масштаб — на 1, вкладка — на ERD, что бы в нем ни было сохранено, а язык Генератора кода, стили имен и тип кавычек — на сохраненных значениях.
В документе, сохраненном до версии 4.2.0, блокировка Name View не указана, поэтому он загружается с включенной блокировкой и в режиме Physical Names, даже если был сохранен со всеми остальными выключенными блокировками.
Начиная с версии 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, а также изменение языка Генератора кода, стиля имен или типа кавычек, пока включена соответствующая блокировка; такое изменение начиная с версии 5.1.0 по-прежнему отправляет только редактор, выпущенный до версии 5.1.0 (см. dispatch, dispatchSync).
Начиная с версии 4.2.0 к таким изменениям относится и переключение режима отображения имен, пока действует блокировка Name View: редактор по-прежнему его отправляет, но реплика его отбрасывает, как и остальные редакторы.
Переключение режима отображения имен, сделанное в редакторе только для чтения, отбрасывается так же, какой бы ни была блокировка.
Событие change элемента не учитывает такое изменение, но реплика все равно о нем сообщает.
В новом документе и в документе, сохраненном до версии 4.0.0, включены все блокировки, а в любом документе, сохраненном до версии 4.2.0, включена блокировка Name View.
Если блокировка Viewport снята, прокрутка или изменение масштаба меняют value, если снята блокировка Canvas Type, value меняет и открытие другой вкладки редактора, а если снята блокировка Name View — переключение режима отображения имен из редактора, в котором можно редактировать.
Включение или выключение блокировки также меняет value.
Начиная с версии 4.0.0 свойство changed сравнивает форму хранения, в которой нет ни удаленных сущностей, ни порядка наложения.
Поэтому таблица, добавленная и снова удаленная в пределах одного события change, правка, попавшая на сущность, которую удалил другой соавтор, и ретранслированная загрузка, меняющая только порядок наложения, тоже дают значение false в свойстве changed.
Начиная с версии 4.2.0 то же значение дает строка, добавленная на лист Словаря и оставленная пустой, так как форма хранения не включает строку, в которой ничего нет.
При вычислении changed сравнение никогда не выполняется с сохраненным файлом, который уже после загрузки может отличаться от value: так происходит с файлом, сохраненным более ранней версией, включая файл v2, а также с файлом, который начинается с метки порядка байтов, и с файлом, отредактированным вручную.
Начиная с версии 4.2.0 к таким файлам относится и файл, сохраненный версией 4.0.0 или 4.1.0, так как value не включает пустые текстовые поля его сущностей и пустые списки doc и коллекции, которые содержит такой файл.
При побайтном сравнении с таким файлом правкой считалась бы даже прокрутка, которую исключает блокировка.
Первое событие 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.2.0 в число принимаемых действий также входят table.changeLogicalName, table.changeNameDerived, column.changeLogicalName, column.changeDomain, column.changeNameDerived, settings.changeNameViewMode, а также dictionary.add, dictionary.update и dictionary.remove, которые передают каждую правку, сортировку и импорт CSV на листах Словаря.
Из переключений режима отображения имен реплика применяет только те, которые делает редактор, в котором можно редактировать, пока блокировка Name View снята, а остальные отбрасывает (см. on@change).
Хранилище более ранней версии отбрасывает все эти действия, поэтому теряет логические имена, привязки к доменам, признак того, сформировано ли каждое имя по словарям, слова, термины и домены, а также режим отображения имен.
Редактор до версии 4.2.0 в сеансе все же может отнять часть этого у реплики версии 4.2.0.
Импорт Схемы SQL, GraphQL, DBML или AML через пункт Import в этом редакторе, но не через Import and Add, очищает слова, термины и домены реплики.
Его отмена загрузки, импорта через пункт Import или вызова clear() загружает в реплику его собственную копию документа без логических имен, привязок к доменам и словарей; см. Разные версии в одном сеансе.
Точка входа @dineug/erd-editor/engine.js экспортирует те же createReplicationStore, ReplicationStore и ReplicationChange, что и раньше, а createReplicationStore принимает тот же контекст.
В версии 5.1.0 не появилось ни нового типа действия, ни изменений в экспорте @dineug/erd-editor/engine.js, поэтому реплика версии 4.2.0 или новее по-прежнему сохраняет каждое изменение, которое отправляет редактор версии 5.1.0.
Начиная с версии 5.1.0 реплика согласует правки, сделанные в один и тот же момент, так же, как редакторы версии 5.1.0; см. Одновременные правки.
Она хранит версию для положения таблицы, для положения и размера заметки и группы таблиц, а также для начала координат вида и уровня масштаба.
Редактор версии 5.1.0 отправляет перетаскивание, перемещение вида или масштабирование как точку, в которой оно закончилось, через table.moveTo, memo.moveTo, tableGroup.moveTo, settings.scrollTo и settings.changeZoomLevel, и реплика принимает каждое из этих действий, а также каждое memo.resize и tableGroup.resize, только если его версия не ниже той, что у нее есть.
Поэтому когда два таких редактора перемещают или изменяют размер одной таблицы, заметки или группы таблиц в один и тот же момент, реплика сохраняет ее в том положении и с тем размером, которые показывает каждый из них, в каком бы порядке она их ни получала, а перетаскивание, перемещение вида или масштабирование, которое транспорт доставил дважды, не сдвигает ничего дважды.
Пока блокировка Viewport снята, реплика сохраняет начало координат и уровень масштаба, беря каждый из них из записи с наибольшей версией, — такими, какими они были у сделавшего ее редактора.
Масштабирование сдвигает и начало координат, поэтому перемещение вида и масштабирование, которые два редактора делают в один и тот же момент, могут сохраниться как начало координат одного редактора с уровнем масштаба другого.
До версии 5.1.0 реплика прибавляла шаги перемещения вида и масштабирования каждого редактора к одному общему виду.
Расстояния, которые редактор, выпущенный до версии 5.1.0, отправляет для перетаскивания, перемещения вида или масштабирования (table.move, memo.move, tableGroup.move, settings.streamScrollTo и settings.streamZoomLevel), по-прежнему прибавляются к тому, что хранит реплика, вне версий.
Изменение цвета по-прежнему приходит без версии: реплика, как и каждый редактор, присваивает ему собственную версию и сохраняет цвет, о котором узнала последним, поэтому две перекраски одной таблицы, заметки или группы таблиц, сделанные в один и тот же момент, могут сохраниться иначе, чем показывает редактор.
Реплика версии до 5.1.0 применяет точки и размеры в порядке получения, поэтому может сохранить таблицу не там, где ее показывают редакторы версии 5.1.0.
Чтобы правки, сделанные в один и тот же момент, приходили к одному результату (за исключением случаев, перечисленных в разделе Одновременные правки), держите каждый редактор сеанса и реплику на версии 5.1.0 или новее.
Начиная с версии 5.1.0 редактор отправляет изменение языка Генератора кода, стиля имен или типа кавычек, только пока блокировка этой настройки выключена, с пометкой shared: true в данных действия, а реплика применяет каждое помеченное изменение, как и редакторы; см. Совместное редактирование.
Непомеченное изменение, которое отправляет редактор, выпущенный до версии 5.1.0, независимо от блокировки, реплика применяет, только пока эта настройка у нее разблокирована, а пока она заблокирована, отбрасывает (см. on@change).
Начиная с версии 4.0.0 оба метода также применяют действие editor.mergeLWW, которое передает регистры совместной работы, отправляемые редактором в ответ на подключение к сеансу другого редактора, поэтому реплика согласует одновременные правки так же, как редакторы.
Оно не меняет ни одного байта value и не вызывает событие change.
Действие settings.changeIgnoreSaveSettings, которое еще может отправлять редактор, выпущенный до версии 4.0.0, принимается, но ничего не меняет: оно вызывает событие change со свойством changed, равным false.
destroy
Полностью уничтожает экземпляр replicationStore.
replicationStore.destroy();
После этого хранилище перестает генерировать событие change и игнорирует дальнейшие отправки действий.
Документ застывает на своем последнем значении, не вызывая ошибку.
Чтобы начать заново, создайте новое хранилище.