@cuboapp/crdt 1.0.27 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client/index.ts +81 -23
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { keyBy, pick, uuid } from '@cuboapp/utils'
|
|
|
2
2
|
import { WsClientEvent } from '@cuboapp/ws'
|
|
3
3
|
import { computed, reactive, shallowReactive } from 'vue'
|
|
4
4
|
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
5
|
-
import { applyUpdate, Doc } from 'yjs'
|
|
5
|
+
import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from 'yjs'
|
|
6
6
|
|
|
7
7
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
8
8
|
import { CuboCrdtServerDocumentIncomingAction } from '../server'
|
|
@@ -586,6 +586,81 @@ export class CuboCrdtClient<M> {
|
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
private sendDocumentUpdate(
|
|
590
|
+
entity: string,
|
|
591
|
+
entity_id: number,
|
|
592
|
+
document: CuboCrdtClientDoc,
|
|
593
|
+
update: Uint8Array,
|
|
594
|
+
origin?: CuboCrdtClientDocOrigin
|
|
595
|
+
) {
|
|
596
|
+
const data: CuboCrdtServerDocumentIncomingAction = {
|
|
597
|
+
action: 'update',
|
|
598
|
+
entity,
|
|
599
|
+
entity_id,
|
|
600
|
+
data: Array.from(update),
|
|
601
|
+
origin: {
|
|
602
|
+
// по умолчанию 'other' — исходную подписку исключаем (её yjs-документ уже
|
|
603
|
+
// применил изменение локально). Без этого апдейты от TipTap/y-prosemirror
|
|
604
|
+
// приходят без expose и сервер эхом шлёт их обратно самому автору.
|
|
605
|
+
expose: 'other',
|
|
606
|
+
...pick(origin || {}, ['store', 'keys', 'expose']),
|
|
607
|
+
subscribe_id: origin?.subscribe_id ?? document.subscribes.values().next().value
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
void this.ws
|
|
612
|
+
.request({
|
|
613
|
+
method: CUBO_CRDT_EVENT.EVENT,
|
|
614
|
+
data
|
|
615
|
+
})
|
|
616
|
+
.catch((error) => console.error('[CRDT] update request failed', error))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Сверка локального документа с состоянием, присланным сервером при повторной подписке
|
|
621
|
+
* (реконнект). Сервер мог за это время пересобрать документ из БД — он выселяет Y.Doc, как
|
|
622
|
+
* только уходит последний подписчик, а обрыв связи это и есть уход. Пересобранный документ
|
|
623
|
+
* ничего не знает о структурах этого клиента, и у обычного CRDT-merge два последствия:
|
|
624
|
+
*
|
|
625
|
+
* 1. При конкурентных set одного ключа строки Yjs выбирает победителя по clientID, а не по
|
|
626
|
+
* времени — локальная (устаревшая) копия могла «победить» строку, сохранённую в БД
|
|
627
|
+
* другим окном. Так в карточке задачи после реконнекта описание откатывалось к версии
|
|
628
|
+
* этого окна.
|
|
629
|
+
* 2. Следующие локальные правки ссылаются на структуры, которых у сервера нет, — Yjs
|
|
630
|
+
* откладывает их в pending навсегда, и всё набранное после реконнекта молча пропадало.
|
|
631
|
+
*
|
|
632
|
+
* Поэтому строка сервера считается истиной: отличающиеся ключи переставляем явно (это
|
|
633
|
+
* причинно позже обеих версий, значит побеждает везде), а серверу досылаем одним апдейтом
|
|
634
|
+
* всё, чего у него нет, — вместе с этой переустановкой, чтобы значение строки на сервере
|
|
635
|
+
* не мигало через устаревшую версию. Без флага store такой апдейт ничего не пишет в БД:
|
|
636
|
+
* значения и так серверные. Локальные правки, набранные во время обрыва, тоже уезжают в
|
|
637
|
+
* этом апдейте.
|
|
638
|
+
*/
|
|
639
|
+
private resyncWithServer(entity: string, entity_id: number, existing: CuboCrdtClientDoc, incoming: Uint8Array) {
|
|
640
|
+
const server = new Doc()
|
|
641
|
+
applyUpdate(server, incoming)
|
|
642
|
+
|
|
643
|
+
const serverRow: Record<string, unknown> = server.getMap().toJSON()
|
|
644
|
+
const map = existing.doc.getMap()
|
|
645
|
+
|
|
646
|
+
// react: false — наружу не шлём по одному, всё уедет ниже одним диффом
|
|
647
|
+
existing.doc.transact(() => {
|
|
648
|
+
for (const [key, value] of Object.entries(serverRow)) {
|
|
649
|
+
if (JSON.stringify(map.get(key)) !== JSON.stringify(value)) {
|
|
650
|
+
map.set(key, value)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}, { react: false })
|
|
654
|
+
|
|
655
|
+
// пустой апдейт кодируется двумя байтами
|
|
656
|
+
const missing = encodeStateAsUpdate(existing.doc, encodeStateVector(server))
|
|
657
|
+
if (missing.length > 2) {
|
|
658
|
+
this.sendDocumentUpdate(entity, entity_id, existing, missing)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
server.destroy()
|
|
662
|
+
}
|
|
663
|
+
|
|
589
664
|
private async onDocumentCreate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
590
665
|
const entity_id = ctx.entity_id
|
|
591
666
|
if (entity_id === undefined) {
|
|
@@ -602,7 +677,10 @@ export class CuboCrdtClient<M> {
|
|
|
602
677
|
}
|
|
603
678
|
|
|
604
679
|
this.attachDocument(ctx.entity, entity_id, existing, opts)
|
|
605
|
-
|
|
680
|
+
|
|
681
|
+
const incoming = new Uint8Array(ctx.data as any)
|
|
682
|
+
applyUpdate(existing.doc, incoming, { react: false })
|
|
683
|
+
this.resyncWithServer(ctx.entity, entity_id, existing, incoming)
|
|
606
684
|
this.applyRowJson(opts.storeKey, entity_id, existing.doc)
|
|
607
685
|
return
|
|
608
686
|
}
|
|
@@ -636,27 +714,7 @@ export class CuboCrdtClient<M> {
|
|
|
636
714
|
|
|
637
715
|
// react = false, если это апдейт с бэка
|
|
638
716
|
if (origin?.react !== false) {
|
|
639
|
-
|
|
640
|
-
action: 'update',
|
|
641
|
-
entity: ctx.entity,
|
|
642
|
-
entity_id,
|
|
643
|
-
data: Array.from(update),
|
|
644
|
-
origin: {
|
|
645
|
-
// по умолчанию 'other' — исходную подписку исключаем (её yjs-документ уже
|
|
646
|
-
// применил изменение локально). Без этого апдейты от TipTap/y-prosemirror
|
|
647
|
-
// приходят без expose и сервер эхом шлёт их обратно самому автору.
|
|
648
|
-
expose: 'other',
|
|
649
|
-
...pick(origin || {}, ['store', 'keys', 'expose']),
|
|
650
|
-
subscribe_id: origin?.subscribe_id ?? document.subscribes.values().next().value
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
void this.ws
|
|
655
|
-
.request({
|
|
656
|
-
method: CUBO_CRDT_EVENT.EVENT,
|
|
657
|
-
data
|
|
658
|
-
})
|
|
659
|
-
.catch((error) => console.error('[CRDT] update request failed', error))
|
|
717
|
+
this.sendDocumentUpdate(ctx.entity, entity_id, document, update, origin)
|
|
660
718
|
}
|
|
661
719
|
})
|
|
662
720
|
|