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

Совместное редактирование

Метод getSharedStore() превращает редактор в узел сеанса совместного редактирования.
Он генерирует каждое изменение в виде потока действий и применяет действия, которые присылают в ответ другие редакторы, — транспорт между ними (WebSocket, WebRTC или любое другое средство, способное передавать JSON) обеспечиваете вы.
Это один из методов ErdEditorElement, и он используется в паре с Удаленным хранением, когда ретранслируемые изменения нужно еще и сохранять.

interface ErdEditorElement extends HTMLElement {
// ...
getSharedStore: (
config?: SharedStoreConfig & {
mouseTracker?: boolean;
focusTracker?: boolean;
}
) => SharedStore;
}

type SharedStoreConfig = {
getNickname?: () => string;
};

type SharedStore = {
connection: () => void;
disconnect: () => void;
dispatch: (actions: Array<AnyAction> | AnyAction) => void;
dispatchSync: (actions: Array<AnyAction> | AnyAction) => void;
subscribe: (fn: (value: AnyAction[]) => void) => Unsubscribe;
flushStreamBuffers: () => void;
destroy: () => void;
};

// example
const sharedStore = editor.getSharedStore();

Действие — это простой сериализуемый объект, описывающий одно изменение документа, поэтому на пути к транспорту оно без потерь проходит через JSON.stringify.
Действия приходят пакетами: метод subscribe передает массив, и этот массив составляет одно целое — ретранслируйте его целиком и целиком отправляйте на другой стороне.
Считайте действие непрозрачным и передавайте его дальше без изменений, не читая и не переписывая, так как именно служебные данные, которые оно несет, поддерживают синхронизацию редакторов.

Часть того, что делается в редакторе, остается в этом редакторе.
Перенос таблицы, заметки или группы таблиц на передний план, который происходит в том числе при выделении, не передается.
Не передается и выбор параметров Statements, Header и Tables на вкладке «Схема SQL»: он относится к окну, в котором сделан.
Начиная с версии 4.1.0 Обозреватель тоже остается в своем редакторе: не передается, открыт ли он, и не передаются его ширина, свернутые строки, строка с фокусом клавиатуры, положение прокрутки дерева, поиск и параметры.
Не передается и подсветка, которую переход к внешнему ключу включает на линии его связи.
Прокрутка, изменение масштаба или переключение вкладки передаются, но никогда не сдвигают вид и не переключают вкладку другого пользователя. Это относится и к прокрутке, которую выполняет переход из Обозревателя, и к переключению на вкладку ERD, когда Обозреватель открывается с другой вкладки.

Блокировки общие: во всех редакторах действует последняя блокировка или разблокировка настройки, а блокировка несет значения редактора, который ее включил, поэтому все редакторы и реплики сохраняют одно и то же значение.
Таким образом, блокировка Viewport сохраняет вид редактора, который ее включил, а блокировка Canvas Type — последнюю вкладку, открытую в этом редакторе перед вкладкой «Настройки», на которой включаются блокировки.
Выключение блокировки Language, Table Name Case, Column Name Case или Bracket Type также устанавливает эту настройку во всех редакторах в значение, отображаемое в редакторе, который ее выключил, поэтому они продолжают сохранять одно значение. Выключение блокировки Viewport или Canvas Type ни у кого не сдвигает вид и не переключает вкладку.

Правка скрипта Before tables или After tables передается целиком, когда его поле теряет фокус. Два скрипта согласуются независимо друг от друга, поэтому правка одного из них соавтором никогда не перезаписывает другой.

Ничто из сделанного в режиме Flow на вкладке «Визуализация» не является изменением документа, поэтому редактор в режиме Flow не отправляет другим никаких изменений, а изменения, которые присылают они, продолжают применяться, пока он там находится.
Единственное исключение — уход из режима Flow на другую вкладку: такое переключение вкладки передается, как и любое другое.
Кнопка карточки со значком внешней ссылки, результат быстрого поиска, выбранный после #, @ или :, и открытие Обозревателя — все это выводит из режима Flow на вкладку ERD. Прокрутка, которую там выполняет кнопка карточки или результат быстрого поиска, тоже передается.

Два редактора на одной странице​

Здесь связка «подписка → отправка» заменяет сеть, поэтому устройство сеанса видно без мешающего транспорта.

const editor1 = document.createElement('erd-editor');
const editor2 = document.createElement('erd-editor');

const sharedStore1 = editor1.getSharedStore({
getNickname: () => 'editor1',
});
const sharedStore2 = editor2.getSharedStore({
getNickname: () => 'editor2',
});

sharedStore1.subscribe(actions => {
sharedStore2.dispatch(actions);
});

sharedStore2.subscribe(actions => {
sharedStore1.dispatch(actions);
});

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

Инициализация нового участника​

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

editor2.setInitialValue(editor1.runtimeValue);

const sharedStore2 = editor2.getSharedStore();

Значение времени выполнения по-прежнему содержит то, что было удалено с момента открытия документа, поэтому, когда другой редактор позже отменяет удаление, новый участник возвращает таблицу вместе с ее именем и столбцами.
Свойство value — это форма, в которой документ хранится в файле, без удаленных сущностей, поэтому новый участник, инициализированный из него или из файла, вместо этого вернул бы пустую безымянную таблицу.
Для сохранения по-прежнему используйте свойство value: свойство runtimeValue нужно только для инициализации другого редактора, второго представления или replicationStore.
Именно так редакторы в VS Code, Obsidian, IDE JetBrains и веб-приложении инициализируют второе представление открытого документа.

Метод setInitialValue не ретранслируется, поэтому инициализация нового участника ничего не меняет у остальных.
Начиная с версии 4.0.0 он также сбрасывает регистры совместной работы редактора, которые согласуют одновременные правки. Если у общего хранилища редактора уже был подписчик, хранилище снова запрашивает регистры у других редакторов, как при первом вызове subscribe, поэтому редактор, заново загруженный посреди сеанса, согласует правки так же, как остальные.
Присваивание свойству value сохраняет регистры, но, в отличие от setInitialValue, ретранслируется: как только у редактора появляется общее хранилище, каждый редактор в сеансе загружает присвоенный документ, поэтому никогда не инициализируйте нового участника таким способом.

Разные версии в одном сеансе​

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

Начиная с версии 3.10.0 изменение ON DELETE или ON UPDATE связи передается как действие одного из двух новых типов, relationship.changeOnDelete и relationship.changeOnUpdate, а действие relationship.add может содержать оба значения. Ретранслятору, передающему действия без изменений, для них ничего нового не нужно.
Редактор, выпущенный до версии 3.10.0, игнорирует эти типы действий и поля: он никогда не узнает ON DELETE или ON UPDATE связи, а в документе, сохраненном из такого редактора, нет ни того ни другого. Связь, добавленная более старым редактором, приходит без них, поэтому оба параметра изначально имеют значение Not set.

Все остальные изменения, которые отправляет редактор версии 3.10.0, передаются в действиях тех типов, которые более старые редакторы уже знают.
Кнопка Replace All в Поиске и замене отправляет свои правки одним пакетом тех же действий переименования, изменения комментариев и заметок, которые отправляет правка вручную. Параметры вида Alternate Key и Referential Actions передаются в существующем действии settings.changeShow.

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

  • добавление, перемещение, изменение размера, переименование, изменение цвета или удаление группы таблиц: tableGroup.add, tableGroup.move, tableGroup.moveTo, tableGroup.resize, tableGroup.changeName, tableGroup.changeColor и tableGroup.remove; добавление таблицы в группу или ее исключение из группы: table.changeGroup;
  • нажатие кнопки Save в окне «Сопоставить столбцы» для существующей связи, которое задает сразу оба ее списка столбцов: relationship.changeColumns;
  • включение или выключение блокировки: settings.changeLockSettings;
  • правка скрипта Before tables или After tables на вкладке «Схема SQL»: settings.changeDDLScript.

Параметры вида Table Groups и Memos передаются в существующем действии settings.changeShow.
Действие settings.changeIgnoreSaveSettings, которое отправляли переключатели Save Scroll Information и Save Zoom Information, пока их не заменили блокировки, больше не отправляется, а действия settings.changeRelationshipOptimization больше нет. Редактор версии 4.0.0, получивший любое из них, ничего не меняет.

Редактор, выпущенный до версии 4.0.0, игнорирует новые типы действий: он никогда не видит ни группу таблиц, ни то, какие таблицы в нее входят, хотя перетаскивание группы все равно перемещает в нем таблицы этой группы, так как оно отправляет и их действия table.move.
После нажатия кнопки Save в окне «Сопоставить столбцы» он оставляет у связи старые столбцы, а когда в нем удаляется столбец из этого старого сопоставления, связь исчезает во всех редакторах. В нем нет ни блокировок, ни скриптов.
В документе, сохраненном из такого редактора, ничего этого нет, и редактор версии 4.0.0 открывает его со всеми включенными блокировками.
Держите все редакторы сеанса и все replicationStore, хранящие его документ, на версии 4.0.0 или новее.

Редактор версии 4.1.0 не отправляет действий новых типов, поэтому редакторы и реплики версий 4.0.0 и 4.1.0 работают в одном сеансе, ничего не пропуская.
Его Поиск и замена ищет также по именам индексов и групп таблиц, а кнопки Replace и Replace All переименовывают их действиями index.changeName и tableGroup.changeName, которые уже применяет редактор версии 4.0.0.

SharedStoreConfig​

getNickname​

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

editor.getSharedStore({
getNickname: () => 'nickname...',
});

mouseTracker​

Транслирует курсор мыши этого пользователя в другие редакторы. Значение по умолчанию — true.
Курсоры, отправленные другими пользователями, всегда отображаются на холсте ERD.

editor.getSharedStore({ mouseTracker: false });

focusTracker​

Транслирует то, с чем работает этот пользователь. Значение по умолчанию — true.
Таблица и ячейка в фокусе, выделенные таблицы, заметки и группы таблиц, а также рамка выделения, которую вы растягиваете на холсте, отправляются в другие редакторы и отображаются там.
Каждому пользователю назначается собственный цвет, чтобы их можно было различить: контур у таблицы в фокусе, подчеркивание у ячейки в фокусе, обводка вокруг выделенных таблиц, заметок и групп таблиц и пунктирный прямоугольник для рамки выделения.
Сведения о присутствии, приходящие от других пользователей, всегда отображаются на вашем холсте независимо от того, включен ли этот параметр.
В режиме Flow на вкладке «Визуализация» фокус и выделение других пользователей отображаются на карточках таблиц, а их рамки выделения — нет. Выделенные вами там таблицы по-прежнему отправляются, а рамка выделения, которую вы там растягиваете, — нет.

editor.getSharedStore({ focusTracker: false });

Выключите оба трекера, чтобы использовать общее хранилище как простой ретранслятор, который не публикует собственных сведений о присутствии.
Сведения о присутствии, отправленные другими редакторами, по-прежнему принимаются и отображаются.
Так делают редакторы в VS Code, Obsidian, IDE JetBrains и Google Drive.

editor.getSharedStore({ mouseTracker: false, focusTracker: false });

Курсор, фокус, выделение и рамка выделения передаются в том же потоке, что и изменения документа, но они эфемерны.
Они никогда не попадают ни в документ, ни в историю изменений, ни в editor.value, поэтому транспорт может свободно их отбрасывать, а replicationStore их игнорирует.

SharedStore​

connection, disconnect​

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

sharedStore.connection();
sharedStore.disconnect();

dispatch, dispatchSync​

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

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

subscribe​

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

const unsubscribe = sharedStore.subscribe(actions => {
// send...
});

flushStreamBuffers​

Отправляет пакеты потоков, которые это хранилище еще удерживает, сразу, а не по истечении периода без изменений.
Начиная с версии 3.9.2 он работает в хранилище, полученном от метода getSharedStore; в версии 3.9.1 этот метод есть в типе, но ничего не делает.
Перемещение таблицы, заметки или группы таблиц, изменение их цвета, изменение размера заметки, а также прокрутка и масштабирование вида приходят потоком мелких изменений. Изменение размера группы таблиц — нет: оно отправляется один раз, когда заканчивается перетаскивание.
Хранилище собирает каждый поток в один пакет и удерживает его, пока в потоке не пройдет 200 мс без изменений. Это происходит в два этапа, поэтому пакет уходит примерно через 400 мс после последнего изменения.
Вызов прекращает это ожидание на обоих этапах. Пока хранилище подключено, подписчик получает пакет до того, как вызов вернет управление. Если удерживать нечего, вызов ничего не отправляет.
Метод destroy, как самого хранилища, так и редактора, отбрасывает пакет, который еще удерживается, поэтому сначала вызовите этот метод, если освобождаете хранилище, а остальные должны сохранить последнее изменение, например при закрытии вкладки.

sharedStore.flushStreamBuffers();
sharedStore.destroy();

destroy​

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

sharedStore.destroy();