Віддалене зберігання
Надсилати весь стан редактора після кожної зміни неефективно.
Для такого випадку редактор надає 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 — це запасний варіант на випадок, коли canvas недоступний.
Його достатньо для сховища, документ якого лише зберігається й ніколи не відображається.
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, тож він завантажується з увімкненим блокуванням 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 зі значенням false у полі changed, тому записуйте лише тоді, коли 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, його змінює й відкриття іншої вкладки редактора, а якщо розблоковано 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 зі значенням true у полі changed повертає такий файл у формі зберігання, тож, записавши це значення, ви один раз перезапишете файл.
Файл, збережений цим випуском, не відрізняється, хоч би де його було збережено, як пояснено в розділі toWidth.
Якщо метод setInitialValue викликано, поки подія change ще очікує, і до того, як настане її час, не передано жодних дій, ця подія change однаково надійде — із завантаженим значенням value і значенням false у полі changed.
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 зі значенням false у полі changed.
destroy
Повністю знищує екземпляр replicationStore.
replicationStore.destroy();
Після цього сховище перестає генерувати подію change та ігнорує подальші передані дії.
Документ застигає на своєму останньому значенні, а не викликає помилку.
Щоб почати знову, створіть нове сховище.