Перейти до основного вмісту

Віддалене зберігання

Надсилати весь стан редактора після кожної зміни неефективно.
Для такого випадку редактор надає 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 — це запасний варіант на випадок, коли canvas недоступний.
Його достатньо для сховища, документ якого лише зберігається й ніколи не відображається.

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 зі значенням false у полі changed, тому записуйте лише тоді, коли changed дорівнює true.
Це прокручування або масштабування, коли заблоковано Viewport, перемикання вкладки, коли заблоковано Canvas Type, а також зміна мови генератора коду, стилю назв або типу лапок, коли ввімкнено власне блокування цього налаштування.
Подія change елемента пропускає таку зміну, але репліка все одно про неї повідомляє.
У новому документі та в документі, збереженому до версії 4.0.0, увімкнено всі блокування.
Якщо Viewport розблоковано, прокручування або масштабування змінює value, а якщо розблоковано Canvas Type, його змінює й відкриття іншої вкладки редактора.
Увімкнення або вимкнення блокування також змінює value.

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

Поле changed ніколи не визначається порівнянням із вашим збереженим файлом, який уже після завантаження може відрізнятися від value: так буває з файлом, збереженим попереднім випуском, зокрема з файлом v2, а також із файлом, що починається з мітки порядку байтів, і з файлом, відредагованим вручну.
Побайтове порівняння з таким файлом зараховувало б як зміну навіть прокручування, яке не враховується через блокування.
Перша подія 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.0.0, обидва методи також застосовують дію editor.mergeLWW, яка передає регістри спільного редагування, якими редактор відповідає, коли до сеансу приєднується інший, тож репліка узгоджує одночасні зміни так само, як редактори.
Вона не змінює жодного байта value і не генерує подію change.
Дія settings.changeIgnoreSaveSettings, яку ще може надсилати редактор, випущений до версії 4.0.0, залишається, але нічого не змінює: вона генерує подію change зі значенням false у полі changed.

destroy​

Повністю знищує екземпляр replicationStore.

replicationStore.destroy();

Після цього сховище перестає генерувати подію change та ігнорує подальші передані дії.
Документ застигає на своєму останньому значенні, а не викликає помилку.
Щоб почати знову, створіть нове сховище.