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

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

Метод getSharedStore() превращает редактор в узел сеанса совместного редактирования.
Он генерирует каждое изменение в виде потока действий и применяет действия, которые присылают в ответ другие редакторы, — транспорт между ними (WebSocket, WebRTC или любое другое средство, способное передавать JSON) обеспечиваете вы.
Это один из методов ErdEditorElement, и он используется в паре с Удаленным хранением, когда ретранслируемые изменения нужно еще и сохранять.
Начиная с версии 5.1.0, чтобы поделиться диаграммой через документ Yjs и любой из его провайдеров, а не настраивать транспорт самостоятельно, привяжите редактор с помощью @dineug/erd-editor-yjs; см. Yjs.

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;
getLWW: () => SharedRegisters;
mergeLWW: (lww: SharedRegisters, version: number) => void;
getVersion: () => number;
forgetPeer: (editorId: string) => void;
reseed: (value: string, options: ReseedOptions) => void;
destroy: () => void;
};

// a JSON object whose shape is the editor's own: hand it on as it is
type SharedRegisters = Record<string, unknown> & {
readonly [registersBrand]: true;
};

type ReseedOptions = {
lww: SharedRegisters;
version: number;
keepScreen?: boolean; // default false
};

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

@dineug/erd-editor не экспортирует ни один из этих типов по имени. Код на TypeScript получает их через ErdEditorElement, например ReturnType<ErdEditorElement['getSharedStore']>.

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

Часть того, что делается в редакторе, остается в этом редакторе.
Перенос таблицы, заметки или группы таблиц на передний план, который происходит в том числе при выделении, не передается.
Не передается и выбор параметров Statements, Header, Write logical names into empty comments (начиная с версии 4.2.0) и Tables на вкладке «Схема SQL»: он относится к окну, в котором сделан.
Начиная с версии 4.1.0 Обозреватель тоже остается в своем редакторе: не передается, открыт ли он, и не передаются его ширина, свернутые строки, строка с фокусом клавиатуры, положение прокрутки дерева, поиск и параметры.
Не передается и подсветка, которую переход к внешнему ключу включает на линии его связи.
Начиная с версии 4.2.0 вкладка «Словарь» тоже остается в своем редакторе: не передается, какой лист открыт, и не передаются поиск, выделение, прокрутка и ширина столбцов каждого листа. Ее правки, сортировки и импорт CSV передаются.
Прокрутка, изменение масштаба или переключение вкладки передаются, но никогда не сдвигают вид и не переключают вкладку другого пользователя. Это относится и к прокрутке, которую выполняет переход из Обозревателя, и к переключению на вкладку ERD, когда Обозреватель открывается с другой вкладки, и к переключению вкладки и прокрутке при переходе с листа Checks вкладки «Словарь» к таблице или столбцу. Переход с этого листа к слову, термину или домену остается на вкладке «Словарь», поэтому из него ничего не передается.

Начиная с версии 4.2.0 передается и переключение режима отображения имен, а блокировка этого режима определяет, что остальные редакторы делают с этим переключением. Блокировка Name View включена в новой диаграмме и в любом документе, созданном до версии 4.2.0.
Пока блокировка включена, переключение отображается только в том редакторе, где оно сделано: оно по-прежнему передается, но все остальные редакторы и все реплики его отбрасывают, поэтому каждый пользователь может смотреть в своем режиме, а свойство value сохраняет заблокированный режим.
Пока блокировка выключена, переключение в редакторе, в котором можно редактировать, меняет режим отображения имен во всех редакторах и репликах, причем действует последнее переключение, и каждый из них его сохраняет.
Собственное переключение редактора только для чтения всегда остается в этом редакторе, а сам он по-прежнему следует общим переключениям остальных.

Начиная с версии 5.1.0 изменение языка Генератора кода, стиля имен таблиц, стиля имен столбцов или типа кавычек подчиняется собственной блокировке (Language, Table Name Case, Column Name Case или Bracket Type) так же, но с одним отличием: пока блокировка включена, изменение не передается вовсе.
Оно отображается только в том редакторе, где сделано, и вкладки «Генератор кода» и «Схема SQL» этого редактора следуют за ним, а другие редакторы и реплики его не видят. Свойства value и runtimeValue сохраняют заблокированное значение, поэтому новый участник, инициализированный из любого редактора, его тоже не видит, и пока блокировка включена, каждый пользователь может смотреть на свой язык, стиль имен или тип кавычек.
Пока блокировка выключена, изменение, сделанное в редакторе, в котором можно редактировать, передается с пометкой «общее» (shared: true в его данных). Оно меняет настройку во всех редакторах и репликах, причем действует последнее изменение, и каждый из них его сохраняет. Редактор или реплика версии 5.1.0 применяет помеченное изменение независимо от собственной блокировки, поэтому изменение, сделанное сразу после разблокировки, доходит везде, даже туда, где приходит раньше разблокировки.
До версии 5.1.0 такое изменение доходило до каждого редактора и реплики независимо от того, включена ли их блокировка, тогда как файл сохранял заблокированное значение.

Начиная с версии 5.0.0 редактор только для чтения не передает никаких изменений. Прокрутка, масштаб, вкладка, режим отображения имен, база данных, язык Генератора кода, стили имен, тип кавычек и строки View Option, которые меняет его читатель, остаются на его экране, что бы ни говорили их блокировки; до версии 5.0.0 его прокрутки, изменения масштаба, переключения вкладок, база данных и настройки кода передавались так же, как у редактора, в котором можно редактировать.
Его хранилище по-прежнему передает то, что передают его трекеры о курсоре, фокусе, выделении и рамке выделения читателя, отвечает на регистры, которые запрашивает присоединяющийся редактор, и применяет все изменения, которые присылают остальные. Изменение базы данных, настройки Генератора кода или строки View Option доходит до его документа, а до его экрана — тоже, если только читатель не менял эту настройку именно там, и общее переключение режима отображения имен в любом случае отображается на его экране. Начиная с версии 5.1.0 он получает настройку Генератора кода, только если она была изменена при выключенной блокировке или ее передает разблокировка.
Когда редактор становится доступным только для чтения, он сначала передает то, что хранилище еще удерживает от перетаскивания или прокрутки, как это делает flushStreamBuffers, чтобы остальные сохранили последнее из этого.

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

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

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

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

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

import '@dineug/erd-editor/erd-editor.js';

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 и не сохраненным файлом.
Комната, привязанная с помощью @dineug/erd-editor-yjs, сама хранит документ после того, как клиент ее инициализировал, поэтому каждый привязанный к ней редактор открывает этот документ без передачи runtimeValue.

editor2.setInitialValue(editor1.runtimeValue);

const sharedStore2 = editor2.getSharedStore();

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

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

Ни value, ни runtimeValue не содержат регистров и версии, которой достигли часы редактора, поэтому новый участник, инициализированный только из runtimeValue, берет их из ответа, который редакторы сеанса присылают на его первую подписку.
Пока этот ответ не пришел, правка нового участника может нести более низкую версию, чем у остальных, поэтому они ее отклоняют, пока она остается только у него, а более старая запись, еще находящаяся в пути, ложится поверх более новой, которая есть в начальных данных. Если ни один работающий редактор не отвечает, например когда документ сеанса хранит сервер или хранилище документов, новый участник их так и не получает.
Начиная с версии 5.1.0 передавайте их вместе со значением. В редакторе сеанса вызовите flushStreamBuffers(), чтобы сначала ушли перетаскивание или изменение цвета, которые он еще удерживает, затем в один и тот же момент получите runtimeValue и результаты методов getLWW() и getVersion() его хранилища. Новый участник загружает значение методом setInitialValue, затем принимает остальное методом mergeLWW своего хранилища, именно в таком порядке, так как каждый setInitialValue сбрасывает регистры. Его следующая правка тогда несет версию выше любой из тех, что есть в начальных данных, поэтому она ложится без ожидания ответа. В уже существующем хранилище reseed делает и то и другое за один вызов.

// on an editor already in the session
sharedStore1.flushStreamBuffers();
const seed = {
value: editor1.runtimeValue,
lww: sharedStore1.getLWW(),
version: sharedStore1.getVersion(),
};

// on the joiner
editor2.setInitialValue(seed.value);
const sharedStore2 = editor2.getSharedStore();
sharedStore2.mergeLWW(seed.lww, seed.version);

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

Одновременные правки​

Каждый редактор и каждая реплика хранят для каждого имени и почти каждого другого поля версию записи, которая его задала: это их регистры совместной работы. Запись ложится, только если ее версия не ниже хранимой, поэтому редакторы, слышащие одни и те же изменения в разном порядке, приходят к одному документу.
Начиная с версии 5.1.0 каждая версия несет под своим счетчиком слот самого записывающего редактора, взятый из его идентификатора. Поэтому версии растут шагами по 4 194 304 и выходят за пределы 32-разрядного целого, когда счетчик достигает 512. JSON.stringify и JSON.parse передают их без потерь, но ретранслятор или хранилище, разбирающие их на другом языке, должны хранить их как 64-разрядные числа, а не как 32-разрядные целые.

Начиная с версии 5.1.0 два редактора, которые меняют одно имя или поле в один и тот же момент, за исключением описанных ниже случаев, приходят к одному значению в каждом редакторе и каждой реплике: записывая при одном счетчике, они ставят две разные версии, и везде побеждает большая. До версии 5.1.0 оба могли поставить одну версию, и каждый редактор сохранял ту, что услышал последней, поэтому они могли разойтись. Два редактора делят слот с вероятностью 1 к 4 194 304 на пару, и тогда, как и раньше, возникает ничья.

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

  • перетаскивание — как один table.moveTo, memo.moveTo или tableGroup.moveTo на каждую перемещенную сущность, в одном пакете, поэтому перетаскивание группы несет точки ее таблиц вместе с точкой группы;
  • отмена или повтор перетаскивания — как точка, в которую при этом ставится каждая сущность;
  • перемещение вида — как settings.scrollTo, а изменение масштаба — как settings.changeZoomLevel;
  • изменение размера заметки или группы таблиц — как ее положение и размер вместе.

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

Изменение цвета — одно из исключений. Оно уходит без версии, и каждый редактор и каждая реплика ставят на него свою и сохраняют цвет, услышанный последним, поэтому два пользователя, которые в один и тот же момент перекрашивают одну таблицу, заметку или группу таблиц, могут получить разные цвета.
Несколько изменений вовсе не имеют версии, поэтому каждый редактор и каждая реплика применяют их в том порядке, в каком слышат, и два таких изменения, сделанные в один и тот же момент, тоже могут разойтись: перемещение столбца внутри его таблицы или индекса (column.move, indexColumn.move), а также изменение базы данных, строки View Option или параметров Relationship DataType Sync, Maximum comment width или Column Order на вкладке «Настройки».
Сущности, которые два пользователя добавляют в один и тот же момент, например две таблицы или два столбца одной таблицы, тоже могут стоять в разном порядке в каждом редакторе и каждой реплике, и свойство value каждого из них сохраняет свой порядок.

Все это верно для редакторов и реплик версии 5.1.0 или новее; что меняет более старая версия, сказано в разделе Разные версии в одном сеансе.

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

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

Начиная с версии 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, игнорирует новые типы действий: он никогда не видит ни группу таблиц, ни то, какие таблицы в нее входят, хотя перетаскивание группы все равно перемещает в нем таблицы этой группы, так как оно отправляет и перемещение каждой из этих таблиц.
После нажатия кнопки 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.

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

Блокировка Name View передается в существующем действии settings.changeLockSettings, параметр вида Domain — в действии settings.changeShow, а перемещение Logical Name или Domain в Порядке столбцов — в действии settings.changeColumnOrder.
Действия editor.clear и editor.loadJson импорта Схемы SQL, GraphQL, DBML или AML через пункт Import содержат keepDictionaries: true, что сохраняет слова, термины и домены во всех редакторах. То же относится к действиям замещающего вызова setSchemaSQL, setSchemaGraphQL, setSchemaDBML или setSchemaAML.
Физическое имя, сформированное словарями, передается в действии table.changeName или column.changeName, а тип данных, который домен задает своим столбцам, — в действии column.changeDataType, то есть в тех же действиях, которые отправляет правка вручную.

Редактор, выпущенный до версии 4.2.0, игнорирует новые типы действий: в нем нет ни логических имен, ни привязок к доменам, ни словарей, ни режима отображения имен. Блокировка или разблокировка Name View ничего в нем не меняет, как и перемещение Logical Name или Domain в Порядке столбцов либо перемещение другого столбца на место одного из них. Ничего не меняет и блокировка Canvas Type, включенная редактором, который перешел на вкладку «Настройки» с вкладки «Словарь»: более старый редактор не знает этой вкладки, поэтому оставляет Canvas Type разблокированным. Он по-прежнему показывает физические имена, сформированные словарями, и типы данных, которые задают домены.
Его собственные правки ничего не знают о логических именах, доменах и словарях, что приводит к следующим последствиям в более новых редакторах:

  • Таблица или столбец, которые он переименовывает вручную, сохраняют пометку о том, что имя сформировано, поэтому логическое имя, позже введенное для них в более новом редакторе, может заново сформировать имя поверх введенного им.
  • Тип данных, который он задает столбцу, привязанному к домену, оставляет привязку на месте, поэтому лист Checks показывает проверку Domain type mismatch, а следующее изменение типа домена снова задает столбцу тип домена.
  • Столбцы внешнего ключа связи, которую он добавляет, не получают от своего ключа ни логического имени, ни домена.
  • Таблица, которую он вставляет или дублирует, приходит без логических имен и привязок к доменам.
  • Импорт Схемы SQL, GraphQL, DBML или AML в нем не отправляет keepDictionaries, поэтому очищает слова, термины и домены во всех редакторах.
  • Его отмена загрузки, импорта или вызова clear() загружает во все редакторы его собственную копию документа, а в этой копии нет ни логических имен, ни привязок к доменам, ни словарей.
  • Эта копия, как и документ, который загружает импорт Схемы SQL, GraphQL, DBML или AML в нем, также возвращает блокировку Name View во всех редакторах в то состояние, в котором ее застал старый редактор. После этого каждый редактор сохраняет в качестве режима отображения имен Physical Names, а при выключенной блокировке и показывает его.

В документе, сохраненном из такого редактора, нет ни логических имен, ни привязок к доменам, ни словарей, ни режима отображения имен, поэтому редактор версии 4.2.0 открывает его в режиме Physical Names. Блокировку Name View более старый редактор все же сохраняет в том состоянии, в котором ее застал, поэтому документ, разблокированный в редакторе версии 4.2.0, остается разблокированным.
Держите все редакторы сеанса и все replicationStore, хранящие его документ, на версии 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, которые, как и любое перемещение и масштабирование, никакой другой редактор не применяет, а берут только реплики. Более старый редактор применяет каждую точку перетаскивания в порядке поступления, вне какого-либо регистра, а более старая реплика делает то же с каждой точкой перетаскивания, перемещения вида или масштабирования, поэтому две точки, отправленные одновременно для одной сущности или для вида, могут там разойтись.
  • Перетаскивание более старого редактора по-прежнему передается как пройденное расстояние, которое каждый редактор и каждая реплика прибавляют вне своих регистров, а его перемещение вида или масштабирование — тоже как расстояние, которое так же прибавляет каждая реплика. Расстояние и точка для одной сущности, сделанные одновременно, могут разойтись в редакторах и репликах, слышащих их в разном порядке, а расстояние, которое транспорт доставляет дважды, прибавляется дважды.
  • За исключениями, которые называет раздел Одновременные правки, запись имени или поля редактором версии 5.1.0, сделанная в один и тот же момент, что и запись того же имени или поля более старым редактором, почти всегда побеждает, с одинаковым результатом в каждом редакторе и каждой реплике. Два более старых редактора по-прежнему могут разойтись из-за одного поля, записанного одновременно.
  • Редактор версии 5.1.0 отправляет изменение языка Генератора кода, стиля имен или типа кавычек, только пока его блокировка выключена, с пометкой shared: true, которую более старый редактор игнорирует, применяя изменение как прежде. Более старый редактор по-прежнему отправляет изменение, сделанное им под блокировкой, без пометки: редактор или реплика версии 5.1.0 отбрасывают его, пока эта настройка там заблокирована, а более старые редакторы применяют. Поэтому под блокировкой более старые редакторы следуют выбору друг друга, а редакторы версии 5.1.0 сохраняют свой, и файл в любом случае сохраняет заблокированное значение. Разблокировка, сделанная в любой версии, несет значение разблокировавшего во все редакторы и реплики.
  • Загрузка, заменяющая документ, например присвоенное value, clear() или импорт с заменой, завершает рисуемую связь в каждом редакторе версии 5.1.0, как в том, где она сделана, так и в каждом, который ее получает. Более старый редактор и после такой загрузки продолжает рисование связи.

Держите все редакторы сеанса и все replicationStore, хранящие его документ, на версии 5.1.0 или новее, чтобы правки, сделанные в один и тот же момент, везде приходили к одному результату, за исключениями, которые называет раздел Одновременные правки; версия 4.2.0 или новее по-прежнему нужна, чтобы сохранялись логические имена и словари. MCP-сервер до версии 1.2.0 здесь считается редактором до версии 5.1.0, а редактор до версии 5.1.0 вообще нельзя привязать к комнате Yjs.

SharedStoreConfig​

getNickname​

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

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

mouseTracker​

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

editor.getSharedStore({ mouseTracker: false });

focusTracker​

Транслирует то, с чем работает этот пользователь. Значение по умолчанию — true.
Таблица и ячейка в фокусе, выделенные таблицы, заметки и группы таблиц, а также рамка выделения, которую вы растягиваете на холсте, отправляются в другие редакторы и отображаются там.
Каждому пользователю назначается собственный цвет, чтобы их можно было различить: контур у таблицы в фокусе, подчеркивание у ячейки в фокусе, обводка вокруг выделенных таблиц, заметок и групп таблиц и пунктирный прямоугольник для рамки выделения.
Начиная с версии 4.2.0 пользователи могут смотреть в разных режимах отображения имен, пока включена блокировка режима отображения имен или после собственного переключения редактора только для чтения. Ячейка в фокусе, которую ваш режим не рисует, подчеркивается на ячейке, которую он рисует на ее месте: имя и логическое имя заменяют друг друга, как и тип данных с доменом.
Сведения о присутствии, приходящие от других пользователей, всегда отображаются на вашем холсте независимо от того, включен ли этот параметр.
В режиме 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​

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

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

subscribe​

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

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

flushStreamBuffers​

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

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

getLWW​

Начиная с версии 5.1.0 возвращает копию регистров совместной работы редактора (см. Одновременные правки).
Копия делается через JSON, поэтому это простой объект, готовый к JSON.stringify, а его изменение оставляет регистры хранилища прежними.
Передавайте его как есть методу mergeLWW или reseed другого хранилища. Его структура задается самим редактором, поэтому не читайте ни одного его поля.
Читайте его вместе с getVersion и runtimeValue редактора в один и тот же момент, чтобы инициализировать нового участника.

const lww = sharedStore.getLWW();

mergeLWW​

Начиная с версии 5.1.0 принимает регистры, выданные другим хранилищем методом getLWW, в регистры этого хранилища, сохраняя большую версию каждого, и принимает version в часы редактора, которые оставляют большее из собственного и переданного значения.
Метод ничего не меняет в документе: ни байта в value или runtimeValue, ни записи в истории отмены, ни события change. Он лишь поднимает версии, которые должна превзойти последующая запись, поэтому после него запись старше переданных регистров отклоняется, а следующая правка редактора несет версию выше version.
Он ничего не отправляет и ни на что не отвечает. Он напрямую принимает то, что первая подписка нового участника получает из ответа остальных.
Вызывайте его после setInitialValue, но никогда до: каждый setInitialValue и каждый reseed сбрасывают хранимые регистры, а вместе с ними и то, что принял mergeLWW до них.

sharedStore.mergeLWW(lww, version);

getVersion​

Начиная с версии 5.1.0 возвращает наибольшую версию, которую учли часы редактора: наибольшую из версий изменений, которые он сделал или принял, запросов регистров и ответов, которыми обмениваются при подключении, и каждой version, переданной в mergeLWW или reseed.
Курсоры, фокус, выделения и рамки выделения, его собственные или чужие, ее не сдвигают.
Передавайте ее вместе с getLWW(), прочитанным в тот же момент, в mergeLWW или reseed нового участника. Это версия, такая же, как у его действий.

const version = sharedStore.getVersion();

forgetPeer​

Начиная с версии 5.1.0 сразу удаляет курсор, фокус, выделение и рамку выделения, которые прислал редактор с указанным идентификатором, вместе с истечением срока, которого ждал каждый из них. Все, что прислали остальные редакторы, остается.
Идентификатор — это meta.editorId, который несет каждое действие, отправляемое общим хранилищем этого редактора. Идентификатор, о котором этот редактор ничего не хранит, ничего не меняет.
Вызывайте метод, когда ваш транспорт узнает, что участник ушел. Иначе то, что прислал участник, остается отображенным до истечения срока: его курсор — через 30 секунд после последнего полученного, его фокус и выделение — через 90 секунд после него, а его рамка выделения — через 3 секунды.

sharedStore.forgetPeer(editorId);

reseed​

Начиная с версии 5.1.0 снова открывает в редакторе документ сеанса, как при повторном подключении. Привязка Yjs вызывает его, чтобы открыть документ комнаты, когда привязывается к комнате, которая уже инициализирована, и снова при каждом повторном подключении, сохраняя экран, если редактор уже присоединялся к комнате.
Метод загружает value так, как это делает setInitialValue, и принимает lww и version так, как это делает mergeLWW, поэтому хранилище затем содержит ровно переданные регистры, а его часы — большее из собственной версии и version. Он также очищает историю отмены и, в отличие от setInitialValue, сбрасывает выделение и фокус, так как они могут указывать на то, чего в значении уже нет.
Кроме формирования физического имени по логическому, описанного ниже, он не создает записи в истории отмены, не вызывает событие change и ничего не отправляет. Если у хранилища уже был подписчик, оно отправляет запрос регистров, который отправляет каждая такая загрузка и который транспорт, уже передавший регистры, может отбросить, а при включенном focusTracker — еще и очищенные выделение и фокус.
Как и setInitialValue, он работает при установленном атрибуте readonly, сбрасывает вид Flow, завершает рисуемую связь и закрывает редакторы заметки, имени группы таблиц и ячейки, сохраняя или отбрасывая набранное в них тем же образом; имя, которое закрывающаяся ячейка логического имени формирует из набранного читателем, — обычная правка: она отправляется, ее можно отменить, и она вызывает событие change.

Без keepScreen экран показывает то, что содержит значение, как после setInitialValue. При keepScreen: true экран читателя накладывается обратно поверх загруженного документа, только на этом экране:

  • положение прокрутки и масштаб, если элемент их уже показывал; ни разу не показанные или загруженные в скрытом состоянии и не показанные с тех пор открываются там, где их ставит значение, как у опоздавшего участника;
  • вкладка, если загрузка ее переключила, а на вкладке «Настройки» — также вкладка, с которой читатель пришел, которую называет блокировка Canvas Type, включенная там;
  • режим отображения имен и каждая настройка Генератора кода, которые читатель показывал на своем экране под своей блокировкой, но только если эта блокировка была включена до загрузки, остается включенной после нее и хранит то же заблокированное значение. Поэтому если за это время участник разблокировал настройку, изменил и снова заблокировал при другом значении, редактор открывается на новом заблокированном значении, а если блокировка выключена до или после загрузки, он показывает загруженное значение;
  • каждая настройка, которую читатель в режиме только для чтения изменил на своем экране, что бы ни говорили блокировки.

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

Вызов reseed не трогает то, что удерживает хранилище. Перетаскивание, перемещение вида или масштабирование, которые еще удерживаются, уходят после него, считанные с повторно инициализированного документа; изменение цвета или изменение размера заметки уходит так, как было сделано, хотя редактор его уже не показывает; а то, что удерживал disconnect(), уходит при connection().
Поэтому сначала вызовите flushStreamBuffers, отправьте то, что он выдал, и повторно инициализируйте значением, которое это содержит. Никогда не вызывайте reseed в отключенном состоянии.

sharedStore.reseed(seed.value, {
lww: seed.lww,
version: seed.version,
keepScreen: true,
});

destroy​

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

sharedStore.destroy();