דילוג לתוכן הראשי

Yjs

@dineug/erd-editor-yjs שומר על סנכרון התרשימים באמצעות מסמך Yjs. זוהי חבילה נפרדת, שפורסמה לראשונה כגרסה 0.1.0 לצד @dineug/erd-editor בגרסה 5.1.0.
bindErdEditor מחבר אלמנט <erd-editor>, ו-bindPeerStore מחבר מאגר עמית ללא ממשק מ-@dineug/erd-editor/peer.js, אל Y.Doc.
כל ספק שנושא את ה-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 הן תלויות עמיתות, ולכן בדף אחד יש עותק אחד מכל אחת: @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 משני מקומות, יש להסיר את הכפילות ב-bundler (Vite: resolve.dedupe: ['yjs']).

החבילה היא ESM בלבד ("type": "module"), עם נקודת כניסה אחת, שורש החבילה, והיא מסומנת כנקייה מתופעות לוואי, ולכן חבילת build שמייבאת רק את 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 אינו מחזיק תמונת מצב, כמו בחדר ריק או לפני שהספק סיים לסנכרן, האלמנט שומר על המסמך שלו, והחיבור אינו כותב דבר לחדר עד שתופיע תמונת מצב, האתחול שלו או של שותף אחר.

bindErdEditor לוקח מהאלמנט מאגר משותף אחד באמצעות getSharedStore, ו-destroy משחרר אותו.
האלמנט חייב להיות כבר <erd-editor>, כשנקודת הכניסה שלו יובאה או ש-defineErdEditor() נקראה – ראו רישום האלמנטים. עד שהתגית מוגדרת, <erd-editor> הוא HTMLElement רגיל, שהדפדפן משדרג כשהתגית מוגדרת והאלמנט נמצא במסמך. עבור אלמנט שעדיין לא שודרג, bindErdEditor זורק TypeError במקום לשגר error.

עריכות החדר מוחלות על האלמנט, ומשגרות change, כמו עריכות של שותף לעריכה, ובהן העריכות שמוחלות מחדש אחרי פתיחת תמונת מצב.
פתיחת תמונת מצב, ברגע החיבור או בהצטרפות מחדש, אינה יוצרת רשומת ביטול, אינה משגרת change ואינה כותבת דבר לחדר, מלבד גזירת השם הלוגי ש-עריכות במצב לא מקוון והצטרפות מחדש מתארת.
כדי לטעון מסמך אחר לחדר שאותחל, יש לבצע שינוי שכל שותף מחיל: להקצות את value, לקרוא ל-clear או לייבא מסמך.
setInitialValue לעולם אינו נכתב לחדר, כי מאגר משותף אינו שולח אותו, ולכן הוא משנה רק את אותו אלמנט, ואילו כל שותף אחר שומר על התרשים של החדר. לאתחול חדר ריק יש להשתמש במקום זאת ב-seed.

Y.Doc אחד מחזיק תרשים אחד. החיבור שומר אותו תחת השמות erd: של ה-Y.Doc וקובע את השדה 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 שנושא סמנים, פוקוס, בחירות ומלבני בחירה, כמו זה של הספק.
באמצעותו החיבור מפעיל את מעקבי העכבר והפוקוס של האלמנט ומציג באלמנט את הנוכחות של שאר השותפים – ראו נוכחות.
בלעדיו מסונכרן רק התרשים: האלמנט אינו שולח סמן, פוקוס או בחירה ואינו מציג את אלה של אחרים.

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 ב-microtask, ולכן מאזין שנוסף מיד אחרי הקריאה שומע אותו, ו-rejoin אינו משוגר בו לעולם.

מאוחר יותר החיבור נעצר כאשר:

  • תמונת המצב של החדר הופכת למבנה שהגרסה הזו אינה קוראת. כל חיבור של הגרסה הזו נעצר, כל עורך שומר על מה שהחיל, ושום עריכה מאוחרת יותר אינה נכתבת;
  • מאגר העמית שלו הושמד תחתיו, ודבר זה מתגלה בפעם הבאה שהחדר משתנה או ש-seed() או compact() נקראים.

חיבור שנעצר משחרר את המאגר המשותף שלקח, ואת המאגר וה-Y.Doc שלו כזוג, כפי ש-destroy עושה, ושומר על המאזינים שלו.

עבור bindPeerStore בלבד, המקרים האלה משגרים error והחיבור ממשיך:

  • compactAfter אינו מספר שלם חיובי: החיבור מקפל רק כשמבקשים ממנו;
  • electionGraceMs או retainMs אינו מספר מ-0 עד 2,147,483,647 ms: ברירת המחדל שלו נכנסת במקומו, כי טיימר מעבר לטווח הזה מופעל מיד;
  • 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'), לאחר ששלח את הגרירות, שינויי הגודל ושינויי הצבע שהעורך שלו עדיין מעכב. קיפול של מה ששותף כבר החיל בלבד אינו פותח דבר מחדש.
הצטרפות מחדש על תמונת מצב של האתחול שהחיבור הצטרף אליו מחילה מחדש כל עריכה שתמונת המצב אינה מחזיקה, ובהן העריכות של השותף עצמו שנעשו בזמן שנעדר, ולכן אף אחת אינה אובדת. הצטרפות מחדש על אתחול אחר אינה שומרת דבר, כפי ש-אתחול חדר מתאר.

כל הצטרפות מחדש מנקה את היסטוריית הביטול, את הבחירה ואת הפוקוס, וסוגרת תצוגת זרימה פתוחה, ציור קשר ועורך של פתק, של שם קבוצת טבלאות או של תא, כי הם עשויים להתייחס למה שתמונת המצב כבר אינה מחזיקה.
שם קבוצת טבלאות שהוקלד ולא אושר נמחק. תא שם לוגי שנסגר גוזר את השם הפיזי משם לוגי שהמשתמש הקליד בו, כשהמסמך שנפתח מחדש עדיין מחזיק אותו, כפי שכל סגירה של התא הזה עושה. הגזירה הזו היא עריכה רגילה: החדר רושם אותה ביומן, היא יוצרת רשומת ביטול והיא משגרת change – ראו reseed.
חיבור שהצטרף לחדר שומר על מה שהמשתמש רואה: מיקום הגלילה, רמת הזום והכרטיסייה, וההגדרות שמשתמש לקריאה בלבד שינה.
הוא שומר גם על תצוגת השמות ועל הגדרות הקוד (שפת הקוד של מחולל הקוד, סגנון השמות וסוג המירכאות) שהמשתמש הציג בעצמו, כל עוד הנעילה של כל אחת מהן הייתה מופעלת לפני ההצטרפות מחדש ועדיין מופעלת אחריה עם אותו ערך נעול.
דבר מכל מה שהוא שומר אינו מגיע ל-value, שמדווח על ההגדרות של המסמך שנפתח מחדש – ראו reseed עם keepScreen.
מיקום הגלילה ורמת הזום של אלמנט שלא הוצג מאז הטעינה האחרונה שלו, והמסך כולו של חיבור שעדיין לא הצטרף, נפתחים במקום שתמונת המצב קובעת.
למאגר עמית אין מסך, ולכן ההצטרפות מחדש שלו אינה שומרת דבר.

עורכים לקריאה בלבד​

אלמנט לקריאה בלבד שמחובר לחדר אינו כותב דבר להיסטוריה של החדר: המאגר המשותף שלו אינו שולח שינוי, רק את המעקבים שלו ואת לחיצת היד של האוגרים, והחדר אינו רושם אף אחד מהם.
הוא עדיין מחיל כל עריכה שהחדר נושא, ועם awareness הסמן, הפוקוס, הבחירה ומלבן הבחירה של המשתמש בו עדיין מגיעים לאחרים.
ההגדרות שהמשתמש משנה נשארות על המסך שלו, כמו בכל עורך לקריאה בלבד.
seed אינו נדחה: אלמנט לקריאה בלבד מאתחל חדר ריק במסמך שהוא מחזיק, או ב-value כשניתן.

מה החדר מחזיק​

החיבור שומר את התרשים בשני שמות של ה-Y.Doc: erd:snapshot, מפה שמחזיקה תמונת מצב של המסמך, ו-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איןהבטחה שנפתרת כשהספק סיים לסנכרן. אף קיפול אינו מתרחש לפניה, גם לא של 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, ולכל היותר 1,000 הרשומות האחרונות שלו, בלי קשר לגודל של כל אחת.
לכן דבר אינו אובד כאשר הדוחס שתמונת המצב שלו מפסידה הוא זה שמתחבר מחדש, כל עוד קיפל לא יותר מ-1,000 רשומות בזמן הפירוד.
כאשר הוא זה שנשאר מקוון ותקופת ההחזקה שלו פגה, תמונת המצב וההיסטוריה של החדר חסרות את מה שקיפל עד שיקפל שוב. שותף שהחמיץ את העריכות האלה, או שותף שמצטרף בינתיים, חסר אותן, ומעכב את העריכות המאוחרות יותר של כל כותב שהדוחס קיפל את העריכות שלו. הקיפול הבא של הדוחס מחזיר את כולן, אלא אם תמונת המצב המנצחת החזיקה עריכות שחסרו לו, ואז הוא נפתח מחדש עליה ומאבד את מה שקיפל לצמיתות.

דוחס מחזיר בקיפול משלו, שנעשה אחרי שהדביק את תמונת המצב המנצחת, בלי בחירה, תקופת המתנה או סף, וכל שותף נפתח מחדש על אותו קיפול באמצעות rejoin('snapshot').
העריכות שהוא מחזיר אינן מגיעות לאף שותף מלבד אותו קיפול, ולכן שותף שהיה מקוון לאורך כל הזמן מסיים באותו מצב כמו האחרים וכמו שותף שמצטרף מאוחר יותר, גם כשעריכה מאוחרת יותר של כותב אחר נשענת על אחת מהן, כגון שינוי שם של טבלה ששותף אחר הוסיף.
כל החזרה עולה לכל שותף את ההצטרפות מחדש הזו, עם מה שהצטרפות מחדש מאבדת. כששני דוחסים מחזירים באותו רגע, כל שותף עדיין מסיים באותו מצב, אם כי עריכה יכולה להישאר מוחלת לפני עריכה שהיא נשענת עליה.
חיבור שהושמד או נעצר לפני שהקיפול הזה רץ, כמו כשמאזין rejoin משמיד אותו, מחזיר במקום זאת את העריכות בנפרד. אף אחת אינה אובדת, אבל אז שותף שהיה מקוון לאורך כל הזמן יכול להחיל אחת אחרי עריכה של כותב אחר שנשענת עליה, ולהסתיים במצב שונה משל שותף שמצטרף מאוחר יותר.

מגבלות​

  • אזור התצוגה נשאר על כל מסך. גלילות, זומים והחלפות כרטיסייה אינם נרשמים לעולם, אף שסשן שמחובר באמצעות getSharedStore שולח אותם (גם שם הם אינם מזיזים את התצוגה או הכרטיסייה של משתמש אחר). כל משתמש שומר את שלו, והחיבור מציג אותו רק במצב ה-Awareness שלו, שדבר עדיין אינו קורא. כשהנעילה Viewport או Canvas Type כבויה, ה-value של כל שותף שומר את שלו; תחת הנעילות, שבמסמך חדש הן מופעלות, כל שותף שומר את הערך הנעול.
  • תמונת המצב גדלה ברשומה אחת לכל חיבור. היא שומרת, לכל חיבור שכתב אי פעם לחדר, עד כמה העריכות שלו מקופלות, ולעולם אינה משמיטה אחד. חיבור שנוצר שוב, או כזה שעבר לאתחול אחר, נספר מחדש, וכך גם כל קיפול שמחזיר. רשומה היא מזהה בן 32 תווים ומספר, ולכן עשרת אלפים סשנים שכתבו מוסיפים כמה מאות קילובייט לכל תמונת מצב ולכל קיפול.
  • hub וחדר אינם מעבירים דבר ביניהם. אלמנט שמחובר לחדר וגם ל-hub של סוכן קוד, או לכל שכבת תעבורה אחרת באמצעות 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.