التحرير التعاوني
تحوّل الدالة getSharedStore() المحرر إلى عقدة في جلسة تعاونية.
فهي تُطلِق كل تغيير على شكل دفق من الإجراءات وتطبّق الإجراءات التي ترسلها المحررات الأخرى — وعليك أنت توفير وسيلة النقل بينها (WebSocket أو WebRTC أو أي وسيلة أخرى يمكنها نقل JSON).
وهي إحدى دوال ErdEditorElement، وتُستخدم مع التخزين البعيد عندما يلزم تخزين التغييرات المُمرَّرة أيضًا.
interface ErdEditorElement extends HTMLElement {
// ...
getSharedStore: (
config?: SharedStoreConfig & {
mouseTracker?: boolean;
focusTracker?: boolean;
}
) => SharedStore;
}
type SharedStoreConfig = {
getNickname?: () => string;
};
type SharedStore = {
connection: () => void;
disconnect: () => void;
dispatch: (actions: Array<AnyAction> | AnyAction) => void;
dispatchSync: (actions: Array<AnyAction> | AnyAction) => void;
subscribe: (fn: (value: AnyAction[]) => void) => Unsubscribe;
flushStreamBuffers: () => void;
destroy: () => void;
};
// example
const sharedStore = editor.getSharedStore();
الإجراء كائن بسيط قابل للتحويل إلى سلسلة يصف تغييرًا واحدًا في المستند، لذا يجتاز JSON.stringify سليمًا في طريقه إلى وسيلة النقل.
وتصل الإجراءات في دفعات: تسلّمك الدالة subscribe مصفوفة، وتلك المصفوفة وحدة واحدة — مرّرها كاملة وأرسلها كاملة في الطرف الآخر.
تعامل مع الإجراء على أنه قيمة مبهمة ومرّره كما هو بدلًا من قراءته أو إعادة كتابته، لأن بيانات التتبع التي يحملها هي ما يُبقي المحررات متزامنة.
بعض ما يجري في المحرر يبقى في ذلك المحرر.
لا يُرسَل إحضار جدول أو ملاحظة أو مجموعة جداول إلى المقدمة، وهو ما يحدث أيضًا عند تحديد أحدها.
ولا تُرسَل كذلك اختيارات Statements وHeader وTables في علامة تبويب مخطط SQL، فهي تخص النافذة التي اختيرت فيها.
منذ الإصدار 4.1.0، يبقى المستكشف أيضًا في محرره: لا يُرسَل ما إذا كان مفتوحًا، ولا مقدار عرضه، ولا صفوفه المطوية، ولا الصف الذي عليه تركيز لوحة المفاتيح، ولا موضع التمرير في شجرته، ولا بحثه وخياراته.
ولا يُرسَل الإبراز الذي يضعه الانتقال إلى مفتاح خارجي على خط الربط الخاص بعلاقته.
يُرسَل التمرير أو تغيير مستوى التكبير أو التبديل بين علامات التبويب، لكنه لا يحرّك أبدًا عرض مستخدم آخر أو علامة تبويبه. ويشمل ذلك التمرير الناتج عن الانتقال من المستكشف، والتبديل إلى علامة تبويب ERD عند فتح المستكشف من علامة تبويب أخرى.
الأقفال مشتركة: آخر قفل لإعداد ما أو إلغاء قفله هو المعتمد في كل المحررات، ويحمل القفل قيم المحرر الذي فعّله، لذا يحفظ كل محرر وكل نسخة متماثلة القيمة نفسها.
لذلك يحفظ قفل Viewport عرض المحرر الذي فعّله، ويحفظ قفل Canvas Type آخر علامة تبويب كان عليها ذلك المحرر قبل علامة تبويب الإعدادات، حيث تُفعَّل الأقفال.
يضبط تعطيل قفل Language أو Table Name Case أو Column Name Case أو Bracket Type ذلك الإعداد أيضًا في كل المحررات على القيمة الظاهرة في المحرر الذي عطّله، فتواصل جميعها حفظ قيمة واحدة. ولا يحرّك تعطيل Viewport أو Canvas Type عرض أحد ولا علامة تبويبه.
يُرسَل تعديل السكربت Before tables أو After tables كاملًا عندما يفقد حقله التركيز. وتُحسم قيمة كل من السكربتين على حدة، لذا لا يكتب تعديل متعاون لأحدهما فوق الآخر أبدًا.
لا شيء مما يُفعل في وضع التدفق في علامة تبويب التصور المرئي يُعد تغييرًا في المستند، لذا لا يرسل المحرر الموجود في وضع التدفق أي تغييرات إلى المحررات الأخرى، وتظل التغييرات التي ترسلها تُطبَّق عليه أثناء وجوده فيه.
الاستثناء الوحيد هو مغادرة وضع التدفق إلى علامة تبويب أخرى: يُرسَل هذا التبديل كأي تبديل آخر بين علامات التبويب.
يؤدي زر الرابط الخارجي في البطاقة، ونتيجة البحث السريع المختارة بعد # أو @ أو :، وفتح المستكشف جميعها إلى مغادرة وضع التدفق إلى علامة تبويب ERD. ويُرسَل أيضًا التمرير الذي يُحدثه هناك زر البطاقة أو نتيجة البحث السريع.
محرران في صفحة واحدة
يحلّ الربط هنا من subscribe إلى dispatch محل الشبكة، لكي ترى بنية الجلسة دون أن تعترضها وسيلة نقل.
const editor1 = document.createElement('erd-editor');
const editor2 = document.createElement('erd-editor');
const sharedStore1 = editor1.getSharedStore({
getNickname: () => 'editor1',
});
const sharedStore2 = editor2.getSharedStore({
getNickname: () => 'editor2',
});
sharedStore1.subscribe(actions => {
sharedStore2.dispatch(actions);
});
sharedStore2.subscribe(actions => {
sharedStore1.dispatch(actions);
});
يبدأ المحرران هنا من المستند الجديد الفارغ نفسه، لذا لا يحتاج أي منهما إلى تهيئة.
مع ثلاثة محررات أو أكثر، أرسل دفعات كل محرر إلى كل المحررات الأخرى: فالمحرر لا يمرّر أبدًا ما استقبله.
تهيئة متعاون
يمرّر المخزن المشترك التغييرات لحظة حدوثها ولا يسلّم المنضم أبدًا المستند بحالته الراهنة، لذا يجب تزويد المحرر الذي ينضم إلى جلسة جارية بالمستند أولًا.
منذ الإصدار 4.0.0، هيّئه بالخاصية runtimeValue من محرر موجود بالفعل في الجلسة، لا بالخاصية value ولا بالملف المحفوظ.
editor2.setInitialValue(editor1.runtimeValue);
const sharedStore2 = editor2.getSharedStore();
لا تزال قيمة وقت التشغيل تحتفظ بما أُزيل منذ فتح المستند، لذا عندما يتراجع محرر آخر لاحقًا عن إزالة، يعيد المنضم الجدول باسمه وأعمدته.
الخاصية value هي الصيغة التي يحملها الملف، دون الكيانات المُزالة، لذا فإن المنضم الذي يُهيَّأ منها، أو من الملف، سيعيد بدلًا من ذلك جدولًا فارغًا بلا اسم.
واصل كتابة value إلى التخزين: فالخاصية runtimeValue مخصصة فقط لتهيئة محرر آخر أو عرض ثانٍ أو replicationStore.
تهيّئ المحررات في VS Code وObsidian وبيئة تطوير JetBrains وتطبيق الويب بهذه الطريقة عرضًا ثانيًا لمستند مفتوح.
لا يُمرَّر استدعاء الدالة setInitialValue، لذا لا تغيّر تهيئة المنضم شيئًا لدى المحررات الأخرى.
منذ الإصدار 4.0.0، تتخلص الدالة أيضًا من مسجّلات التعاون في المحرر، وهي التي تحسم التعديلات المتزامنة. وإذا سبق أن اشترك أحد في المخزن المشترك للمحرر، يطلب المخزن من المحررات الأخرى مسجّلاتها مجددًا، كما في أول استدعاء للدالة subscribe، لكي يحسم المحرر المُعاد تحميله في منتصف الجلسة التعديلات كما تحسمها المحررات الأخرى.
أما إسناد قيمة إلى value فيُبقي على المسجّلات، لكنه بخلاف setInitialValue يُمرَّر: فبمجرد أن يصبح للمحرر مخزن مشترك، يحمّل كل محرر في الجلسة المستند المُسنَد، لذا لا تهيّئ منضمًا بهذه الطريقة أبدًا.
الجمع بين إصدارات مختلفة
لا يطبّق المحرر إلا أنواع الإجراءات التي يعرفها إصداره، لذا في الجلسة التي تجمع بين إصدارات مختلفة تفوت المحررات الأقدم كل ما يُنقل عبر نوع أحدث.
منذ الإصدار 3.10.0، يُنقل تغيير ON DELETE أو ON UPDATE لعلاقة ما عبر أحد نوعين جديدين من الإجراءات، هما relationship.changeOnDelete وrelationship.changeOnUpdate، ويمكن أن يحمل relationship.add كليهما. ولا يحتاج وسيط تمرير ينقل الإجراءات كما هي إلى أي شيء جديد من أجلها.
يتجاهل المحرر الصادر قبل الإصدار 3.10.0 أنواع الإجراءات والحقول تلك: فلا يعرف أبدًا ON DELETE أو ON UPDATE لعلاقة ما، والمستند المحفوظ منه لا يحتوي على أي منهما. والعلاقة التي يضيفها محرر أقدم تصل دونهما، لذا يبدأ كلاهما بالقيمة Not set.
ويُنقل كل تغيير آخر يرسله محرر بالإصدار 3.10.0 عبر أنواع إجراءات تعرفها المحررات الأقدم بالفعل.
يرسل Replace All في البحث والاستبدال تعديلاته في دفعة واحدة من إجراءات إعادة التسمية والتعليق والملاحظة نفسها التي يرسلها التعديل اليدوي. ويُنقل خيارا العرض Alternate Key وReferential Actions عبر الإجراء الموجود مسبقًا settings.changeShow.
منذ الإصدار 4.0.0، تُنقل هذه التغييرات عبر أنواع إجراءات جديدة، ولا يحتاج وسيط تمرير ينقل الإجراءات كما هي إلى أي شيء جديد من أجلها:
- إضافة مجموعة جداول أو نقلها أو تغيير حجمها أو إعادة تسميتها أو تغيير لونها أو إزالتها:
tableGroup.addوtableGroup.moveوtableGroup.moveToوtableGroup.resizeوtableGroup.changeNameوtableGroup.changeColorوtableGroup.remove؛ وانضمام جدول إلى مجموعة أو مغادرته لها:table.changeGroup؛ - النقر على
Saveفي تعيين الأعمدة لعلاقة موجودة، وهو ما يضبط قائمتي أعمدتها كلتيهما في آن واحد:relationship.changeColumns؛ - تفعيل قفل أو تعطيله:
settings.changeLockSettings؛ - تعديل السكربت
Before tablesأوAfter tablesفي علامة تبويب مخطط SQL:settings.changeDDLScript.
ويُنقل خيارا العرض Table Groups وMemos عبر الإجراء الموجود مسبقًا settings.changeShow.
لم يعد يُرسَل settings.changeIgnoreSaveSettings، الذي كان يرسله مفتاحا التبديل Save Scroll Information وSave Zoom Information قبل أن تحل الأقفال محلهما، وأُزيل settings.changeRelationshipOptimization. ولا يغيّر المحرر بالإصدار 4.0.0 شيئًا إذا استقبل أيًا منهما.
يتجاهل المحرر الصادر قبل الإصدار 4.0.0 أنواع الإجراءات الجديدة: فلا يرى أبدًا أي مجموعة جداول ولا الجداول التي تضمها، مع أن سحب المجموعة لا يزال يحرّك جداولها لديه، لأن السحب يرسل أيضًا إجراءات table.move الخاصة بها.
ويُبقي على الأعمدة القديمة للعلاقة بعد النقر على Save في تعيين الأعمدة، وعندما يُزال لديه عمود من ذلك التعيين القديم، تختفي العلاقة من كل المحررات. وليس لديه أقفال ولا سكربتات.
والمستند المحفوظ من محرر كهذا لا يحتوي على أي من ذلك، ويفتحه المحرر بالإصدار 4.0.0 وجميع الأقفال مفعّلة.
احرص على أن يكون كل محرر في الجلسة، وأي replicationStore يخزّن مستندها، على الإصدار 4.0.0 أو أحدث.
لا يرسل المحرر بالإصدار 4.1.0 أي نوع إجراء جديد، لذا تتشارك المحررات والنسخ المتماثلة بالإصدارين 4.0.0 و4.1.0 الجلسة نفسها دون أن يفوتها شيء.
وتبحث ميزة البحث والاستبدال فيه أيضًا في أسماء الفهارس وأسماء مجموعات الجداول، ويعيد Replace وReplace All تسميتها عبر الإجراءين index.changeName وtableGroup.changeName اللذين يطبّقهما المحرر بالإصدار 4.0.0 بالفعل.
SharedStoreConfig
getNickname
تضبط الاسم المستعار الذي يظهر بجوار مؤشر الماوس الخاص بهذا المستخدم في المحررات الأخرى.
يرى المستخدمون الآخرون user عندما يكون الاسم المستعار مفقودًا أو فارغًا.
editor.getSharedStore({
getNickname: () => 'nickname...',
});
mouseTracker
يبث مؤشر الماوس الخاص بهذا المستخدم إلى المحررات الأخرى. القيمة الافتراضية true.
تظهر المؤشرات التي يرسلها المستخدمون الآخرون دائمًا على لوحة رسم ERD.
editor.getSharedStore({ mouseTracker: false });
focusTracker
يبث ما يعمل عليه هذا المستخدم. القيمة الافتراضية true.
يُرسَل إلى المحررات الأخرى ويُرسم فيها كل من الجدول والخلية اللذين عليهما تركيزك، والجداول والملاحظات ومجموعات الجداول التي حددتها، والمربع الذي تسحبه على لوحة الرسم.
يحصل كل مستخدم على لون خاص به لكي تميّز بين المستخدمين: إطار حول الجدول الذي عليه التركيز، وخط سفلي تحت الخلية التي عليها التركيز، وحلقة حول الجداول والملاحظات ومجموعات الجداول المحددة، ومستطيل متقطع لمربع السحب.
تُرسم معلومات الحضور الواردة من المستخدمين الآخرين دائمًا على لوحة الرسم لديك، سواء أكان هذا الخيار مفعّلًا أم لا.
في وضع التدفق في علامة تبويب التصور المرئي، يُرسم تركيز المستخدمين الآخرين وتحديدهم على بطاقات الجداول، أما مربعات السحب الخاصة بهم فلا تُرسم. ولا تزال الجداول التي تحددها هناك تُرسَل؛ أما المربع الذي تسحبه هناك فلا يُرسَل.
editor.getSharedStore({ focusTracker: false });
عطّل كلا المتتبعين لاستخدام المخزن المشترك كوسيط تمرير بسيط لا ينشر أي معلومات حضور خاصة به.
ولا تزال معلومات الحضور التي ترسلها المحررات الأخرى تُستقبل وتُرسم.
وهذا ما تفعله المحررات في VS Code وObsidian وبيئة تطوير JetBrains وGoogle Drive.
editor.getSharedStore({ mouseTracker: false, focusTracker: false });
يُنقل المؤشر والتركيز والتحديد ومربع السحب عبر الدفق نفسه الذي تمر عبره تغييرات المستند، لكنها عابرة.
فهي لا تدخل أبدًا في المستند ولا في سجل التراجع ولا في editor.value، لذا يمكن لوسيلة النقل أن تُسقطها، ويتجاهلها أي replicationStore.
SharedStore
connection, disconnect
تضبط هاتان الدالتان حالة الاتصال الحالية.
أثناء قطع الاتصال، تُحفظ التغييرات في مخزن مؤقت داخلي.
تُطلَق التغييرات المخزنة مؤقتًا إلى المشتركين بمجرد أن يصبح الإرسال ممكنًا مجددًا.
الحالة الافتراضية هي حالة الاتصال (connection).
sharedStore.connection();
sharedStore.disconnect();
dispatch, dispatchSync
تطبّق التغييرات الواردة من مثيلات المحرر الأخرى على مثيل المحرر الحالي.
sharedStore.dispatch(actions); // async
sharedStore.dispatchSync(actions); // sync
subscribe
تشترك في تغييرات المحرر.
لا يخرج شيء من المحرر حتى يوجد مشترك واحد على الأقل، وتُحفظ التغييرات التي تحدث قبل ذلك في مخزن مؤقت.
ويطلب أول اشتراك أيضًا من المحررات الأخرى مسجّلات التعاون الخاصة بها، وهي التي تحسم التعديلات المتزامنة، لكي يحسمها المنضم المتأخر بالطريقة نفسها.
لكنه لا يجلب المستند، لذا هيّئ المنضم المتأخر أولًا.
منذ الإصدار 4.0.0، يطلب المخزن ذلك مجددًا في كل مرة تحمّل فيها الدالة setInitialValue مستندًا في محرره بعد ذلك الاشتراك الأول.
const unsubscribe = sharedStore.subscribe(actions => {
// send...
});
flushStreamBuffers
ترسل دفعات الدفق التي لا يزال هذا المخزن يحتجزها، الآن، بدلًا من إرسالها بعد انقضاء فترة هدوئها.
منذ الإصدار 3.9.2، تعمل على المخزن الناتج عن getSharedStore؛ وفي الإصدار 3.9.1 تكون الدالة موجودة في النوع لكنها لا تفعل شيئًا.
يصل كل من نقل جدول أو ملاحظة أو مجموعة جداول، وتغيير لون أي منها، وتغيير حجم ملاحظة، وتمرير العرض أو تغيير مستوى تكبيره، على شكل دفق من التغييرات الصغيرة. ولا ينطبق ذلك على تغيير حجم مجموعة جداول: فهو يُرسَل مرة واحدة، عند انتهاء السحب.
يجمع المخزن كل دفق في دفعة واحدة ويحتجزها حتى يهدأ الدفق لمدة 200 مللي ثانية. ويفعل ذلك على مرحلتين، لذا تخرج الدفعة بعد نحو 400 مللي ثانية من آخر تغيير.
ينهي الاستدعاء ذلك الانتظار في المرحلتين كلتيهما. وما دام المخزن متصلًا، يستلم المشترك الدفعة قبل أن يعود الاستدعاء. ولا يرسل الاستدعاء شيئًا إذا لم يكن هناك شيء محتجز.
تُسقط الدالة destroy، سواء أكانت دالة المخزن نفسه أم دالة المحرر، أي دفعة لا تزال محتجزة، لذا استدع هذه الدالة أولًا عندما تتخلى عن مخزن ويجب أن تحتفظ المحررات الأخرى بآخر تغيير، كما عند إغلاق علامة تبويب.
sharedStore.flushStreamBuffers();
sharedStore.destroy();
destroy
تدمّر مثيل sharedStore بالكامل.
ولا يتأثر المحرر نفسه، لذا يظل المستند قابلًا للتحرير بعد مغادرة الجلسة.
يتوقف متتبعا الماوس والتركيز بمجرد تدمير آخر sharedStore سلّمه المحرر.
وتُسقط معه أي دفعة دفق لا يزال المخزن يحتجزها؛ استدع flushStreamBuffers أولًا لإرسالها.
sharedStore.destroy();