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

Yjs

@dineug/erd-editor-yjs синхронизирует диаграммы через документ Yjs. Это отдельный пакет, впервые выпущенный как 0.1.0 рядом с @dineug/erd-editor 5.1.0.
bindErdEditor привязывает к Y.Doc элемент <erd-editor>, а bindPeerStore — автономное одноранговое хранилище из @dineug/erd-editor/peer.js.
Диаграмму переносит любой провайдер, который переносит Y.Doc: websocket-сервер, WebRTC, IndexedDB или ваш собственный. Провайдер и Y.Doc остаются вашими: привязка никогда не подключает, не отключает и не уничтожает их.

Правки передаются как собственные пакеты действий редактора, которые привязка хранит в том виде, в каком их отправило хранилище.
Поэтому регистры совместной работы редактора согласуют правки, сделанные в один и тот же момент, так же, как в сеансе, подключенном через getSharedStore, и с теми же исключениями, например изменением цвета — см. Одновременные правки.
Курсоры, фокус, выделения и рамки выделения передаются через Awareness провайдера — см. Присутствие.
По сравнению с самостоятельным подключением общего хранилища к транспорту привязка добавляет следующее:

  • Подключившийся клиент открывает диаграмму, которую хранит комната, без передачи runtimeValue из работающего редактора.
  • Клиент, потерявший связь, продолжает редактировать, и его правки попадают в комнату после повторного подключения — см. Правки офлайн и повторное подключение.
  • Y.Doc, возвращенный провайдером постоянного хранения, открывает диаграмму, которую хранит, — см. Что хранит комната.
  • Необязательный вспомогательный процесс на Node сворачивает историю комнаты в один снимок — см. Сжатие комнаты.

Привязке нужны пять методов, добавленных в общее хранилище и в одноранговое хранилище в версии 5.1.0 — getLWW, mergeLWW, getVersion, forgetPeer и reseed, — и все нужные вызовы она делает сама.

Установка​

npm install @dineug/erd-editor-yjs @dineug/erd-editor yjs y-protocols
npm install y-websocket # or another provider

@dineug/erd-editor, yjs и y-protocols — это peer-зависимости, поэтому на странице находится по одной копии каждого пакета: @dineug/erd-editor версии 5.1.0 или новее в пределах 5.x, yjs версии 13.6 или новее в пределах 13.x и y-protocols версии 1.0 или новее в пределах 1.x.
Пакет берет из них только типы и работает с переданными вами Y.Doc и Awareness, поэтому не загружает собственный код Yjs, y-protocols или редактора.
Две копии Yjs на одной странице ломают проверки конструкторов друг друга, и вторая выводит в журнал Yjs was already imported. Если ваш код и провайдер подключают yjs из разных мест, устраните дублирование в сборщике (Vite: resolve.dedupe: ['yjs']).

Пакет поставляется только как ESM ("type": "module"), с одной точкой входа — корнем пакета, и помечен как не имеющий побочных эффектов, поэтому сборка, импортирующая только bindErdEditor, не включает сжимающий процесс.
bindErdEditor требует DOM, так как привязывает определенный элемент <erd-editor>. bindPeerStore работает и в обычном Node, на Node.js 22.12 или новее, как и вспомогательный процесс сжатия.

Использование​

function bindErdEditor(
editor: ErdEditorElement,
doc: Y.Doc,
options?: BindOptions
): ErdYjsBinding;
import '@dineug/erd-editor/erd-editor.js';

import { bindErdEditor } from '@dineug/erd-editor-yjs';
import { WebsocketProvider } from 'y-websocket';
import * as Y from 'yjs';

const doc = new Y.Doc();
const provider = new WebsocketProvider('wss://yjs.example.com', 'my-diagram', doc);
const editor = document.createElement('erd-editor');
document.body.appendChild(editor);

provider.on('sync', function onSync(synced) {
if (!synced) return;
provider.off('sync', onSync);

const binding = bindErdEditor(editor, doc, {
awareness: provider.awareness,
getNickname: () => 'Ada',
});
binding.on('error', error => console.error(error));
binding.on('rejoin', reason => console.info('reopened on the room', reason));
binding.seed(); // false, and nothing written, once the room holds a diagram
});

wss://yjs.example.com и my-diagram обозначают ваш собственный сервер и комнату.
Привязывайте и инициализируйте комнату только после синхронизации провайдера, о которой сообщает событие sync y-websocket со значением true. Инициализация, выполненная раньше, может заменить диаграмму комнаты — см. Инициализация комнаты.

Если в момент привязки Y.Doc хранит снимок комнаты, привязка сразу открывает его в элементе: она загружает снимок вместе с его регистрами и версией документа, как это делает reseed, и применяет все правки, записанные в журнал после него.
Это открытие очищает и закрывает то же, что и повторное подключение — см. Правки офлайн и повторное подключение, — а элемент открывается там, где его располагает снимок. Для этого первого открытия rejoin не возникает.
Если Y.Doc не хранит снимок, как в пустой комнате или до синхронизации провайдера, элемент сохраняет свой документ, а привязка ничего не записывает в комнату, пока не появится снимок — собственный, созданный seed, или снимок другого клиента.

bindErdEditor берет у элемента одно общее хранилище через getSharedStore, а destroy освобождает его.
Элемент уже должен быть <erd-editor>: его точка входа импортирована или вызван defineErdEditor() — см. Регистрация элементов. Пока тег не определен, <erd-editor> остается обычным HTMLElement, который браузер обновляет, как только тег определен, а элемент находится в документе. Для еще не обновленного элемента bindErdEditor выбрасывает TypeError, а не генерирует error.

Правки комнаты применяются к элементу и вызывают change, как правки соавтора, и среди них правки, воспроизведенные после открытия снимка.
Открытие снимка, при привязке или при повторном подключении, не создает записи в истории отмены, не вызывает change и ничего не записывает в комнату, за исключением формирования физического имени по логическому, которое описано в разделе Правки офлайн и повторное подключение.
Чтобы загрузить другой документ в инициализированную комнату, сделайте изменение, которое применяют все клиенты: присвойте value, вызовите clear или импортируйте документ.
setInitialValue в комнату никогда не записывается, так как общее хранилище его не отправляет, поэтому он меняет только этот элемент, а все остальные клиенты сохраняют диаграмму комнаты. Пустую комнату инициализируйте с помощью seed.

Один Y.Doc хранит одну диаграмму. Привязка держит ее под именами Y.Doc с префиксом erd: и задает поле erd локального состояния Awareness, а все остальные имена и поля не трогает.
Дайте каждому привязанному элементу собственные Y.Doc и Awareness, как у каждого клиента комнаты: две привязки с общим Awareness перезаписывают поле erd друг друга.

Инициализация комнаты​

В комнате нет диаграммы, пока клиент не инициализирует ее.
То, что редактор хранит до этого, попадает в его собственную инициализацию и уступает инициализации другого клиента, если та пришла раньше: привязка заново открывается на ней с rejoin('snapshot'), а документ, который хранил элемент, вместе с правками, сделанными в нем после привязки, пропадает.

Когда два клиента инициализируют комнату одновременно, на всех клиентах побеждает одна инициализация.
Другой инициализировавший клиент заново открывается на ней с rejoin('seedLost') и теряет правки, сделанные после своей инициализации, как и клиент, подключившийся к проигравшей инициализации, — с rejoin('snapshot'). Их правки в проигравшей инициализации не доходят ни до кого.
Инициализация, выполненная до синхронизации провайдера, считается выполненной в один и тот же момент с инициализацией, из которой получена диаграмма комнаты, поэтому она может победить и заменить диаграмму комнаты.

BindOptions​

import type { Awareness } from 'y-protocols/awareness';

type BindOptions = {
awareness?: Awareness;
getNickname?: () => string;
};

awareness​

Awareness, который переносит курсоры, фокус, выделения и рамки выделения, например Awareness провайдера.
С ним привязка включает трекеры мыши и фокуса элемента и рисует на элементе присутствие других клиентов — см. Присутствие.
Без него синхронизируется только диаграмма: элемент не отправляет ни курсор, ни фокус, ни выделение и не рисует присутствие других.

bindErdEditor(editor, doc, { awareness: provider.awareness });

getNickname​

Имя, которое другие видят рядом с курсором этого редактора.
Общее хранилище элемента читает его при каждой отправке, как и в случае с getSharedStore, а если имя отсутствует или пусто, другие пользователи видят user.

bindErdEditor(editor, doc, {
awareness: provider.awareness,
getNickname: () => 'nickname...',
});

ErdYjsBinding​

type ErdYjsBinding = {
seed: (value?: string) => boolean;
on: <K extends keyof BindingEvents>(
event: K,
listener: BindingEvents[K]
) => () => void;
destroy: () => void;
};

type BindingEvents = {
rejoin: (reason: RejoinReason) => void;
error: (error: Error) => void;
};

type RejoinReason = 'snapshot' | 'seedLost';

on(event, listener) возвращает функцию, которая удаляет обработчик.

seed​

Записывает первый снимок комнаты из документа редактора и возвращает true.
Если передан value — текст документа в любом виде, который читает setInitialValue, — сначала загружает его в редактор.
Затем без записи в журнал отпускает перетаскивания, изменения размера и изменения цвета, которые редактор еще удерживает, чтобы ни одно из них не попало в комнату после снимка. Без value снимок их уже отражает, так как берет документ редактора; с value они относились к документу, который заменил value.
После этого записывает снимок, который описан в разделе Что хранит комната.
Если комната уже хранит снимок, возвращает false и ничего не записывает, не загружая value, поэтому его может вызвать каждый клиент после привязки. Также возвращает false, если привязка уничтожена или остановлена, и для привязки, отклоненной при создании.

binding.seed(); // the document the editor holds
// or
binding.seed(await (await fetch('/diagrams/shop.erd.json')).text());

on@rejoin​

Привязка заново открыла редактор на снимке комнаты, как это делает reseed; что при этом сохраняется, а что теряется, сказано в разделе Правки офлайн и повторное подключение.
Причину сообщает reason:

  • 'snapshot': снимок содержит правки, которые привязка не применила, потому что отсутствовала, пока сжимающий процесс сворачивал их, или потому что сжимающий процесс вернул в собственном сворачивании то, что уже свернул в снимок, а его заменил снимок другого сжимающего процесса (см. Один сжимающий процесс на комнату). Это также причина, когда снимок — первый, который дошел до привязки пустой комнаты, и когда это снимок другой инициализации, а эта привязка подключилась к инициализации, которую не записывала.
  • 'seedLost': инициализация другого клиента победила инициализацию, записанную этой привязкой.
binding.on('rejoin', reason => {
console.info('reopened on the room', reason);
});

on@error​

Привязка отказалась привязываться, остановилась или отклонила параметр. Каждое сообщение начинается с erd-editor-yjs: .

При создании привязка ничего не привязывает, когда:

  • редактор или одноранговое хранилище уже привязано к этому Y.Doc;
  • его @dineug/erd-editor собран до версии 5.1.0; привязка уничтожает общее хранилище, которое взяла у элемента;
  • одноранговое хранилище было уничтожено до привязки;
  • комната хранит снимок в структуре, которую эта версия не читает (см. Что хранит комната).

Такая привязка генерирует error в микрозадаче, поэтому обработчик, добавленный сразу после вызова, его услышит, а rejoin для нее не возникает никогда.

Позже привязка останавливается, когда:

  • снимок комнаты меняется на структуру, которую эта версия не читает. Останавливаются все привязки этой версии, каждый редактор сохраняет то, что успел применить, и ничего, отредактированное после этого, не записывается;
  • ее одноранговое хранилище было уничтожено, пока она работала, что обнаруживается при следующем изменении комнаты или вызове seed() либо compact().

Остановившаяся привязка освобождает взятое ею общее хранилище, а также свое хранилище и Y.Doc как пару, как это делает destroy, и сохраняет свои обработчики.

Только для bindPeerStore следующие случаи генерируют error, а привязка продолжает работу:

  • compactAfter не является положительным целым числом: привязка сворачивает только по запросу;
  • electionGraceMs или retainMs не является числом от 0 до 2 147 483 647 мс: вместо него действует значение по умолчанию, так как таймер за пределами этого диапазона срабатывает сразу;
  • synced отклоняется: привязка никогда не сворачивает и оставляет выборы другим сжимающим процессам (см. Один сжимающий процесс на комнату).
binding.on('error', error => {
console.error(error.message);
});

destroy​

Отправляет в комнату перетаскивания, изменения размера и изменения цвета, которые редактор еще удерживает, задает полю Awareness erd этой привязки значение null, чтобы остальные сразу убрали ее курсор, фокус и выделение, и прекращает наблюдать за Y.Doc и Awareness.
Для элемента также уничтожает взятое привязкой общее хранилище, оставляя элемент и его документ как есть.
Повторный вызов ничего не делает. После этого правки редактора не доходят ни до кого.

Одноранговое хранилище, Y.Doc и провайдер остаются вашими, и уничтожать их нужно после привязки: одноранговое хранилище, уничтоженное первым, никогда не отправит перетаскивания, которые еще удерживало.
destroy самого элемента уничтожает все выданные им общие хранилища, включая хранилище привязки, и сбрасывает то, что они удерживают, поэтому сначала уничтожайте привязку.
Уничтожайте привязку, прежде чем снова привязывать тот же редактор или то же одноранговое хранилище к тому же Y.Doc; привязка, остановленная из-за error, уже освободила их.

binding.destroy();
editor.destroy();

Присутствие​

С awareness курсор редактора, таблица или ячейка в фокусе, выделение и рамка выделения передаются в поле erd локального состояния его Awareness, но не в Y.Doc, поэтому они никогда не попадают в историю комнаты, в ее снимок или в сохраненный Y.Doc.
Остальные рисуют их так же, как присутствие соавтора — см. focusTracker, — а подключившийся клиент рисует присутствие, которое комната уже показывает.
Элемент заново отправляет свой фокус и выделение каждые 30 секунд, а рамку выделения — каждую секунду. Каждая из этих повторных отправок доходит до остальных, даже когда повторяет предыдущую, поэтому то, что пользователь еще удерживает, никогда не пропадает.

Ушедший клиент забывается сразу.
Когда Awareness убирает его состояние, или его поле erd становится null, как это делает destroy, либо называет другой редактор, привязка удаляет курсор, фокус, выделение и рамку выделения этого редактора из элемента с помощью forgetPeer, не дожидаясь истечения их срока.
Она сохраняет их, пока другой клиент все еще показывает тот же редактор, как элемент, снова привязанный к другому Y.Doc той же комнаты.

Правки офлайн и повторное подключение​

Клиент, потерявший связь с провайдером, продолжает редактировать. Его правки ждут в локальном Y.Doc, а не в общем хранилище, и доходят до остальных, когда провайдер снова подключается и синхронизируется.

Если сжимающий процесс свернул правки, которые клиент не применил, пока отсутствовал, они содержатся в снимке, поэтому при повторном подключении клиент заново открывается на снимке с rejoin('snapshot'), предварительно отправив перетаскивания, изменения размера и изменения цвета, которые его редактор еще удерживал. Сворачивание только того, что клиент уже применил, ничего заново не открывает.
Повторное подключение к снимку той инициализации, к которой подключилась привязка, воспроизводит все правки, которых нет в снимке, включая собственные правки этого клиента, сделанные в его отсутствие, поэтому ничто не теряется. Повторное подключение к другой инициализации ничего не сохраняет, как описано в разделе Инициализация комнаты.

Каждое повторное подключение очищает историю изменений, выделение и фокус и закрывает открытый режим Flow, рисование связи и поле редактирования заметки, имени группы таблиц или ячейки, так как они могут указывать на то, чего в снимке больше нет.
Имя группы таблиц, которое набрано, но еще не подтверждено, отбрасывается. Ячейка логического имени, которая закрывается, формирует физическое имя из логического имени, набранного читателем, если открытый заново документ еще хранит его, как и при любом закрытии этой ячейки. Это формирование — обычная правка: комната записывает ее в журнал, она создает запись в истории отмены и вызывает change — см. reseed.
Привязка, которая уже подключилась к комнате, сохраняет то, что видит читатель: положение прокрутки, масштаб и вкладку, а также настройки, которые изменил читатель в режиме только для чтения.
Она также сохраняет режим отображения имен и настройки кода (язык Генератора кода, стиль имен и тип кавычек), которые читатель показывал сам, если блокировка каждой из них была включена до повторного подключения и остается включенной после него с тем же заблокированным значением.
Ничто из сохраненного не попадает в value, который сообщает настройки открытого заново документа — см. reseed с keepScreen.
Положение прокрутки и масштаб элемента, который не показывался с момента последней загрузки, а также весь экран привязки, которая еще не подключилась, открываются там, где их располагает снимок.
У однорангового хранилища нет экрана, поэтому при его повторном подключении ничего не сохраняется.

Редакторы только для чтения​

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

Что хранит комната​

Привязка хранит диаграмму под двумя именами Y.Doc: erd:snapshot — это map со снимком документа, а erd:log — массив пакетов действий редактора после него. Провайдер постоянного хранения сохраняет оба вместе с остальным Y.Doc.

Снимок содержит runtimeValue документа, включая удаленные сущности, регистры совместной работы и версию документа, а также то, насколько далеко правки каждого автора свернуты в него. Его записывает инициализация, а также каждое сворачивание сжимающего процесса.
Каждая запись журнала — это один пакет в том виде, в каком его отправил редактор, вместе с метаданными, которые содержат идентификатор редактора и псевдоним отправителя.
Прокрутки, изменения масштаба, переключения вкладок, присутствие и обмен регистрами в журнал не записываются никогда. Все остальные изменения, которые отправляет редактор, записываются, включая переключение режима отображения имен, сделанное при включенной блокировке этого режима, которое все остальные клиенты отбрасывают.
Язык Генератора кода, стиль имен или тип кавычек, измененные при включенной блокировке, не отправляются никогда, поэтому остаются на экране этого редактора и не доходят до комнаты.

Y.Doc, возвращенный провайдером постоянного хранения, открывает диаграмму, которую хранит, как привязка открывает любой Y.Doc со снимком — см. Использование.

Снимок не является файлом: его значение времени выполнения содержит удаленные сущности, которых в файле быть не должно.
Чтобы записать диаграмму комнаты в хранилище, сохраняйте value привязанного элемента каждый раз, когда возникает его событие change.
Сохраняйте его также один раз после привязки, после seed с переданным value и в обработчике rejoin, так как ни одна из этих загрузок не вызывает change.
Элемент только для чтения не вызывает change никогда, поэтому сохраняйте из элемента, который не находится в режиме только для чтения, либо читайте value из привязанного однорангового хранилища, которое применяет правки комнаты по мере их поступления.
replicationStore привязать нельзя, так как у него нет методов, нужных привязке, а хранилище, питаемое от общего хранилища привязанного элемента, пропускает правки комнаты — см. Репликация работающего редактора.

Структура закрытая и может измениться вместе с ERD_YJS_FORMAT.
Читайте комнату только через привязку, а имена с префиксом erd: и поле Awareness erd оставьте ей.

Сжатие комнаты​

История комнаты растет с каждой правкой, и каждый подключающийся клиент воспроизводит ее целиком, пока что-нибудь ее не свернет.
bindPeerStore привязывает автономное одноранговое хранилище из @dineug/erd-editor/peer.js и возвращает ErdYjsPeerBinding: те же seed, on и destroy, а также compact.
Одноранговое хранилище, привязанное в процессе Node рядом с сервером, — вспомогательный процесс, — сворачивает историю в снимок своей диаграммы.
Привязка элемента никогда не сворачивает, а одноранговое хранилище, привязанное без compactAfter и без вызова compact(), — обычный автономный участник.

function bindPeerStore(
peer: PeerStore,
doc: Y.Doc,
options?: PeerBindOptions
): ErdYjsPeerBinding;

type ErdYjsPeerBinding = ErdYjsBinding & {
compact: () => CompactResult;
};
npm install @dineug/erd-editor-yjs @dineug/erd-editor yjs y-protocols y-websocket ws
import { createPeerStore } from '@dineug/erd-editor/peer.js';
import { bindPeerStore } from '@dineug/erd-editor-yjs';
import WebSocket from 'ws';
import { WebsocketProvider } from 'y-websocket';
import * as Y from 'yjs';

const doc = new Y.Doc();
const provider = new WebsocketProvider('wss://yjs.example.com', 'my-diagram', doc, {
WebSocketPolyfill: WebSocket,
});
const synced = new Promise(resolve => {
provider.on('sync', function onSync(isSynced) {
if (!isSynced) return;
provider.off('sync', onSync);
resolve();
});
});

const peer = createPeerStore({ nickname: 'compactor', presence: false });
const binding = bindPeerStore(peer, doc, {
awareness: provider.awareness,
compactAfter: 1000,
synced,
});
binding.on('error', error => console.error(error));

process.on('SIGTERM', () => {
binding.destroy(); // first, so the peer store's held drags still reach the room
peer.destroy();
provider.destroy();
doc.destroy(); // ends the provider's Awareness timer, so the process can exit
});

Одноранговое хранилище остается вашим: привязка никогда его не уничтожает. Его псевдоним — тот, что был передан в createPeerStore, а presence: false не дает ему отправлять собственный фокус.

Сворачивание записывает снимок из значения времени выполнения, регистров и версии документа самого однорангового хранилища и удаляет записи журнала, которые оно охватывает, и все записи другой инициализации, — все в одной транзакции.
Клиенты, применившие все, видят, как записи исчезают; клиент, который не применил, заново открывается на снимке (см. Правки офлайн и повторное подключение).
Каждое сворачивание переписывает всю диаграмму в снимок, поэтому каждое отправляет каждому клиенту обновление размером примерно со значение времени выполнения диаграммы в JSON и регистры вместе, сколько бы записей оно ни охватило. Задайте compactAfter достаточно большим, чтобы записи, которые охватывает сворачивание, перевешивали одну копию диаграммы.

PeerBindOptions​

type PeerBindOptions = CompactOptions & {
awareness?: Awareness;
};

type CompactOptions = {
compactAfter?: number;
synced?: Promise<void>;
electionGraceMs?: number;
retainMs?: number;
};
ПараметрПо умолчаниюОписание
awarenessНетПоказывает одноранговое хранилище в Awareness комнаты, как awareness делает это для элемента. Сжимающий процесс, который сворачивает сам, помечается там, поэтому сжимающие процессы, видящие друг друга, выбирают один для сворачивания, а тот, который не видит других клиентов, сам ничего не сворачивает — см. Один сжимающий процесс на комнату. Без него сжимающий процесс сворачивает сам, без выборов и без периода ожидания.
compactAfterНетСворачивать самостоятельно, когда журнал содержит столько записей, сколько охватило бы сворачивание. Без него или без synced сворачивает только compact(), а также сворачивание, которое возвращает (см. Один сжимающий процесс на комнату).
syncedНетPromise, который выполняется, когда провайдер синхронизировался. До этого сворачивания не происходит, включая compact(). Без него ничего не сворачивается самостоятельно, а compact() сворачивает сразу.
electionGraceMs3000С awareness — как долго сжимающий процесс наблюдает за комнатой, прежде чем свернуть сам, после synced и снова всякий раз, когда другие клиенты появляются после того, как их не было.
retainMs600000 (десять минут)Как долго записи сворачивания остаются удержанными для возврата (см. Один сжимающий процесс на комнату), считая с первого обновления, которое комната отправляет после сворачивания.

Недопустимый параметр или отклоненный synced генерирует error.

compact​

type CompactResult =
| { compacted: true; generation: number }
| {
compacted: false;
reason: 'not-synced' | 'no-snapshot' | 'destroyed';
};

Сворачивает сразу, без выборов, периода ожидания и порога.
Сначала принимает текущий снимок комнаты, включая повторное подключение, и отправляет то, что его одноранговое хранилище еще удерживает.
Отвечает { compacted: true, generation }, где generation считает сворачивания снимка: 0 для инициализации и на единицу больше при каждом сворачивании.
В остальных случаях отвечает { compacted: false, reason }:

  • 'destroyed', когда привязка уничтожена или остановлена, и для привязки, отклоненной при создании, даже до выполнения synced;
  • 'not-synced' до выполнения synced;
  • 'no-snapshot' в комнате, которую не инициализировал ни один клиент.
const result = binding.compact();
if (!result.compacted) console.warn(result.reason);

Один сжимающий процесс на комнату​

Запускайте ровно один сжимающий процесс на комнату и запускайте новый только после остановки старого. Ничто не принуждает к этому.
Среди сжимающих процессов, видящих друг друга в Awareness, сам сворачивает только тот, у кого наименьший идентификатор клиента Yjs, а когда он уходит или его synced отклоняется, следующий подхватывает работу сразу.
Сжимающий процесс, в Awareness которого нет других клиентов, например когда его собственный сокет закрывается и убирает их всех, сам ничего не сворачивает, пока другие не появятся снова, а он не понаблюдает за комнатой в течение electionGraceMs.

Два сжимающих процесса, потерявших связь друг с другом, или перезапуск, при котором предшественник еще работает, все равно могут свернуть каждый свое.
Тогда комната сохраняет один из двух снимков, а сжимающий процесс, чей снимок проиграл, возвращает то, что он свернул, а победитель не содержит, пока удерживает это: retainMs и не более его последних 1000 записей, каким бы ни был размер каждой.
Поэтому ничто не теряется, когда проигравший снимок принадлежит сжимающему процессу, который подключился заново, при условии, что за время разрыва он свернул не более 1000 записей.
Если это тот, что оставался в сети, а его удержание истекло, в снимке комнаты и ее истории нет того, что он свернул, пока он не свернет снова. Клиент, пропустивший эти правки, или клиент, подключившийся за это время, их не получает и удерживает последующие правки каждого автора, чьи правки сжимающий процесс свернул. Следующее сворачивание сжимающего процесса возвращает их все, если только выигравший снимок не содержал правок, которых у него не было: тогда он заново открывается на этом снимке и безвозвратно теряет то, что свернул.

Сжимающий процесс возвращает правки в собственном сворачивании, выполненном после того, как он догнал выигравший снимок, без выборов, периода ожидания и порога, и каждый клиент заново открывается на этом сворачивании с rejoin('snapshot').
Возвращаемые правки доходят до клиентов только вместе с этим сворачиванием, поэтому клиент, остававшийся в сети все время, приходит к тому же результату, что и остальные, и клиент, подключившийся позже, даже когда более поздняя правка другого автора опирается на одну из них, например переименование таблицы, добавленной другим клиентом.
Каждый такой возврат обходится каждому клиенту в это повторное подключение, с тем, что оно теряет. Когда два сжимающих процесса возвращают правки одновременно, все клиенты все равно приходят к одному результату, хотя правка может остаться примененной раньше той, на которую опирается.
Привязка, уничтоженная или остановленная до выполнения этого сворачивания, например когда ее уничтожает обработчик rejoin, возвращает правки отдельно. Ничто не теряется, но тогда клиент, остававшийся в сети все время, может применить одну правку после правки другого автора, которая на нее опирается, и разойтись с подключившимся позже.

Ограничения​

  • Область просмотра остается на каждом экране. Прокрутки, изменения масштаба и переключения вкладок никогда не записываются в журнал, хотя сеанс, подключенный через getSharedStore, их отправляет (там они тоже не двигают ни вид, ни вкладку другого пользователя). Каждый читатель сохраняет свои, а привязка показывает их только в своем состоянии Awareness, которое пока ничто не читает. Когда блокировка Viewport или Canvas Type выключена, value каждого клиента сохраняет собственные значения; при включенных блокировках, которые у нового документа включены, каждый клиент сохраняет заблокированное значение.
  • Снимок растет на одну запись на каждую привязку. Для каждой привязки, когда-либо писавшей в комнату, он хранит, насколько далеко свернуты ее правки, и никогда не удаляет запись. Заново созданная привязка или привязка, перешедшая на другую инициализацию, считается заново, как и каждое сворачивание, возвращающее правки. Запись — это 32-символьный идентификатор и число, поэтому десять тысяч сеансов записи добавляют несколько сотен килобайт к каждому снимку и каждому сворачиванию.
  • Хаб и комната ничего не ретранслируют друг другу. Элемент, привязанный к комнате и к хабу агента для программирования либо к любому другому транспорту через getSharedStore(), применяет правки обоих, но ни один не слышит правки другого, так как общее хранилище никогда не передает дальше то, что получило.
  • Одновременные добавления могут оказаться в разном порядке. Таблицы или заметки, добавленные двумя клиентами одновременно, могут стоять в диаграмме каждого клиента в другом порядке, как в сеансе, подключенном через getSharedStore, — см. Одновременные правки.
  • Цвета и несколько других одновременных изменений могут дать разный результат. Изменение цвета записывается в журнал без версии, и каждый клиент ставит на него свою при применении. Поэтому две одновременные перекраски одной таблицы, заметки или группы таблиц могут оставить клиентов, а также клиента, подключившегося позже и воспроизводящего журнал, с разными цветами. То же относится к другим изменениям, которые каждый клиент применяет в том порядке, в каком их услышал, например к двум перемещениям столбцов в одной таблице или индексе либо двум изменениям базы данных — см. Одновременные правки.
  • Курсор ушедшего клиента зависит от сервера. Привязка убирает курсор участника, как только Awareness его удаляет. Сервер, который при закрытии сокета удаляет только те состояния Awareness, что этот сокет добавил первыми, оставляет клиента, подключившегося заново, на экранах остальных после его ухода, пока Awareness не удалит его по тайм-ауту, через 30 секунд. Сервер, который удаляет каждое состояние, записанное сокетом последним, — как обновление, так и добавление, — убирает курсор сразу.

Экспорт​

ЭкспортОписание
bindErdEditor(editor, doc, options?)Привязывает элемент <erd-editor> — см. Использование. Возвращает ErdYjsBinding.
bindPeerStore(peer, doc, options?)Привязывает одноранговое хранилище из @dineug/erd-editor/peer.js — см. Сжатие комнаты. Возвращает то же с compact (ErdYjsPeerBinding).
ERD_YJS_FORMATСтруктура Y.Doc, которую записывает эта версия, 1; привязка отклоняет комнату, записанную в другой.

Типы: BindOptions, PeerBindOptions, CompactOptions, CompactResult, BindingEvents, RejoinReason, ErdYjsBinding, ErdYjsPeerBinding.