@cuboapp/crdt 1.0.22 → 1.0.24
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 +129 -149
- package/src/client/types/document.ts +6 -0
- package/src/client/types/index.ts +3 -9
- package/src/client/types/store.ts +1 -7
- package/src/server/document/index.ts +31 -154
- package/src/server/index.ts +11 -54
- package/src/server/types/document.ts +0 -14
- package/src/server/utils/index.ts +0 -41
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { keyBy, pick, uuid } from '@cuboapp/utils'
|
|
2
2
|
import { WsClientEvent } from '@cuboapp/ws'
|
|
3
|
-
import { computed, reactive
|
|
3
|
+
import { computed, reactive } from 'vue'
|
|
4
4
|
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
5
5
|
import { applyUpdate, Doc } from 'yjs'
|
|
6
6
|
|
|
7
7
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
8
8
|
import { CuboCrdtServerDocumentIncomingAction } from '../server'
|
|
9
|
-
import {
|
|
10
|
-
CuboCrdtKey,
|
|
11
|
-
CuboCrdtListSyncData,
|
|
12
|
-
CuboCrdtSubscriptionStatus
|
|
13
|
-
} from '../types'
|
|
9
|
+
import { CuboCrdtKey, CuboCrdtListSyncData, CuboCrdtSubscriptionStatus } from '../types'
|
|
14
10
|
|
|
15
11
|
import { AsyncSerialQueue } from './queue'
|
|
16
12
|
import {
|
|
13
|
+
CuboCrdtClientDoc,
|
|
17
14
|
CuboCrdtClientDocOrigin,
|
|
18
15
|
CuboCrdtClientDocUpdateOptions,
|
|
19
16
|
CuboCrdtClientList,
|
|
@@ -28,6 +25,7 @@ export * from './types'
|
|
|
28
25
|
|
|
29
26
|
export class CuboCrdtClient<M> {
|
|
30
27
|
public store: CuboCrdtClientStore<M> = {}
|
|
28
|
+
public docs = new Map<string, CuboCrdtClientDoc>()
|
|
31
29
|
|
|
32
30
|
private listeners: Map<string, Map<string, (ctx: CuboCrdtClientSubscribeEvent) => void | Promise<void>>> = new Map()
|
|
33
31
|
|
|
@@ -234,8 +232,7 @@ export class CuboCrdtClient<M> {
|
|
|
234
232
|
|
|
235
233
|
this.store[storeKey] = {
|
|
236
234
|
state,
|
|
237
|
-
|
|
238
|
-
awarenesses: shallowReactive(new Map())
|
|
235
|
+
awarenesses: new Map()
|
|
239
236
|
}
|
|
240
237
|
}
|
|
241
238
|
|
|
@@ -260,6 +257,7 @@ export class CuboCrdtClient<M> {
|
|
|
260
257
|
}
|
|
261
258
|
|
|
262
259
|
// подписываемся на фронте
|
|
260
|
+
// console.log('subscribe', opts)
|
|
263
261
|
this.listeners.get(entity)?.set(subscribe_id, async (ctx) => {
|
|
264
262
|
await this.onIncomingUpdate(ctx, {
|
|
265
263
|
subscribe_id,
|
|
@@ -330,13 +328,11 @@ export class CuboCrdtClient<M> {
|
|
|
330
328
|
|
|
331
329
|
// отписываемся на бэке
|
|
332
330
|
if (this.ws.connected) {
|
|
333
|
-
void this.ws
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
})
|
|
331
|
+
void this.ws.request({ method: CUBO_CRDT_EVENT.UNSUBSCRIBE, data: { subscribe_id } }).catch((e) => {
|
|
332
|
+
if (this.debug) {
|
|
333
|
+
console.warn('[CRDT] unsubscribe request failed (socket down)', subscribe_id, e)
|
|
334
|
+
}
|
|
335
|
+
})
|
|
340
336
|
}
|
|
341
337
|
|
|
342
338
|
// отписываемся на фронте
|
|
@@ -358,8 +354,18 @@ export class CuboCrdtClient<M> {
|
|
|
358
354
|
a.destroy()
|
|
359
355
|
})
|
|
360
356
|
item.awarenesses?.clear()
|
|
361
|
-
|
|
362
|
-
item.
|
|
357
|
+
|
|
358
|
+
item.state.rows?.map((row) => {
|
|
359
|
+
const doc = this.docs.get(`${entity}:${(row as any).id}`)
|
|
360
|
+
if (doc) {
|
|
361
|
+
doc.subscribes.delete(subscribe_id)
|
|
362
|
+
|
|
363
|
+
if (doc.subscribes?.size === 0) {
|
|
364
|
+
doc.doc.destroy()
|
|
365
|
+
this.docs.delete(`${entity}:${(row as any).id}`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
})
|
|
363
369
|
}
|
|
364
370
|
|
|
365
371
|
this.store[storeKey] = undefined
|
|
@@ -383,7 +389,6 @@ export class CuboCrdtClient<M> {
|
|
|
383
389
|
subscribe,
|
|
384
390
|
upgrade,
|
|
385
391
|
unsubscribe,
|
|
386
|
-
docs: computed(() => this.store[storeKey]?.docs),
|
|
387
392
|
awarenesses: opts?.awareness ? computed(() => this.store[storeKey]?.awarenesses) : undefined,
|
|
388
393
|
subscribed: () => computed(() => this.store[storeKey]?.state.subscribed || false),
|
|
389
394
|
loading: () => computed(() => this.store[storeKey]?.state.loading || false),
|
|
@@ -404,30 +409,18 @@ export class CuboCrdtClient<M> {
|
|
|
404
409
|
): CuboCrdtClientRow<T> {
|
|
405
410
|
// console.log('store key', opts?.storeKey ?? `${entity}:${id}`)
|
|
406
411
|
|
|
407
|
-
const {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
upgrade,
|
|
414
|
-
unsubscribe,
|
|
415
|
-
subscribed,
|
|
416
|
-
loading,
|
|
417
|
-
ready,
|
|
418
|
-
status,
|
|
419
|
-
error
|
|
420
|
-
} = this.useList(entity, {
|
|
421
|
-
...opts,
|
|
422
|
-
filters: opts?.filters ?? { id },
|
|
423
|
-
storeKey: opts?.storeKey ?? `${entity}:${id}`
|
|
424
|
-
})
|
|
412
|
+
const { subscribe_id, storeKey, awarenesses, subscribe, upgrade, unsubscribe, subscribed, loading, ready, status, error } =
|
|
413
|
+
this.useList(entity, {
|
|
414
|
+
...opts,
|
|
415
|
+
filters: opts?.filters ?? { id },
|
|
416
|
+
storeKey: opts?.storeKey ?? `${entity}:${id}`
|
|
417
|
+
})
|
|
425
418
|
|
|
426
419
|
return {
|
|
427
420
|
subscribe_id,
|
|
428
421
|
storeKey,
|
|
429
|
-
doc:
|
|
430
|
-
awareness:
|
|
422
|
+
doc: this.docs.get(`${entity}:${id}`)?.doc,
|
|
423
|
+
awareness: computed(() => awarenesses?.value?.get(id)),
|
|
431
424
|
subscribe,
|
|
432
425
|
upgrade,
|
|
433
426
|
unsubscribe,
|
|
@@ -450,51 +443,47 @@ export class CuboCrdtClient<M> {
|
|
|
450
443
|
dto: K extends Extract<keyof M, string> ? Partial<M[K]> : object,
|
|
451
444
|
opts?: CuboCrdtClientDocOrigin
|
|
452
445
|
) {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
446
|
+
const storeKey = `${entity}:${entity_id}`
|
|
447
|
+
|
|
448
|
+
const cardRow = this.store[storeKey]?.state.rows?.[0]
|
|
449
|
+
const listRow = this.store[entity]?.state?.rows.find((r: any) => r.id === entity_id)
|
|
450
|
+
const doc = this.docs.get(storeKey)
|
|
451
|
+
|
|
452
|
+
// обновляем документ
|
|
453
|
+
if (doc) {
|
|
454
|
+
const map = doc.doc.getMap()
|
|
455
|
+
|
|
456
|
+
// "или" - потому что они синхронизированы (без разницы какой брать)
|
|
457
|
+
const row = (cardRow || listRow) as any
|
|
458
|
+
const toUpdate = Object.fromEntries(Object.entries(dto).filter(([key, value]) => row[key] !== value))
|
|
459
|
+
|
|
460
|
+
if (Object.keys(toUpdate).length) {
|
|
461
|
+
const origin: CuboCrdtClientDocOrigin = {
|
|
462
|
+
store: opts?.store ?? true,
|
|
463
|
+
expose: opts?.expose ?? 'other',
|
|
464
|
+
keys: opts?.keys ?? Object.keys(toUpdate),
|
|
465
|
+
react: true // отправляем на бэк
|
|
466
|
+
}
|
|
475
467
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
468
|
+
doc.doc.transact(() => {
|
|
469
|
+
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
470
|
+
map.set(key, value)
|
|
471
|
+
})
|
|
472
|
+
}, origin)
|
|
473
|
+
}
|
|
481
474
|
}
|
|
482
475
|
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
488
|
-
map.set(key, value)
|
|
489
|
-
})
|
|
490
|
-
}, origin)
|
|
491
|
-
|
|
492
|
-
const row = store.state.rows.find((item: any) => Number(item.id) === Number(entity_id))
|
|
476
|
+
// обновляем реактивку
|
|
477
|
+
for (const row of [cardRow, listRow]) {
|
|
478
|
+
if (row) {
|
|
479
|
+
const toUpdate = Object.fromEntries(Object.entries(dto).filter(([key, value]) => row[key as keyof typeof row] !== value))
|
|
493
480
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
481
|
+
if (Object.keys(toUpdate).length) {
|
|
482
|
+
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
483
|
+
row[key as keyof typeof row] = value
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
}
|
|
498
487
|
}
|
|
499
488
|
}
|
|
500
489
|
|
|
@@ -555,54 +544,13 @@ export class CuboCrdtClient<M> {
|
|
|
555
544
|
}
|
|
556
545
|
}
|
|
557
546
|
|
|
558
|
-
private ensureDocumentAwareness(
|
|
559
|
-
ctx: CuboCrdtClientSubscribeEvent,
|
|
560
|
-
opts: CuboCrdtClientDocUpdateOptions,
|
|
561
|
-
doc: Doc,
|
|
562
|
-
entity_id: number
|
|
563
|
-
) {
|
|
564
|
-
if (!opts.awareness) {
|
|
565
|
-
return
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
let awareness = this.store[opts.storeKey]?.awarenesses.get(entity_id)
|
|
569
|
-
|
|
570
|
-
if (awareness) {
|
|
571
|
-
return
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
awareness = new Awareness(doc)
|
|
575
|
-
|
|
576
|
-
awareness.on('update', ({ added, updated, removed }, origin) => {
|
|
577
|
-
if (origin === 'remote') {
|
|
578
|
-
return
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const changed = added.concat(updated).concat(removed)
|
|
582
|
-
const update = encodeAwarenessUpdate(awareness!, changed)
|
|
583
|
-
|
|
584
|
-
this.ws.request({
|
|
585
|
-
method: CUBO_CRDT_EVENT.EVENT,
|
|
586
|
-
data: {
|
|
587
|
-
action: 'awareness',
|
|
588
|
-
entity: ctx.entity,
|
|
589
|
-
entity_id,
|
|
590
|
-
data: Array.from(update),
|
|
591
|
-
origin: { expose: 'other', subscribe_id: opts.subscribe_id }
|
|
592
|
-
}
|
|
593
|
-
})
|
|
594
|
-
})
|
|
595
|
-
|
|
596
|
-
this.store[opts.storeKey]?.awarenesses.set(entity_id, awareness)
|
|
597
|
-
}
|
|
598
|
-
|
|
599
547
|
private async onDocumentCreate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
600
548
|
const entity_id = ctx.entity_id
|
|
601
549
|
if (entity_id === undefined) {
|
|
602
550
|
return
|
|
603
551
|
}
|
|
604
552
|
|
|
605
|
-
const existing = this.
|
|
553
|
+
const existing = this.docs.get(`${ctx.entity}:${ctx.entity_id}`)
|
|
606
554
|
if (existing) {
|
|
607
555
|
// документ уже есть локально (типичный кейс — повторный SUBSCRIBE после реконнекта).
|
|
608
556
|
// вместо игнорирования вмёрживаем входящее состояние в существующий yjs-документ
|
|
@@ -611,13 +559,12 @@ export class CuboCrdtClient<M> {
|
|
|
611
559
|
console.log('[CRDT] onDocumentCreate: doc already exists, merging state', { ctx, opts })
|
|
612
560
|
}
|
|
613
561
|
|
|
614
|
-
applyUpdate(existing, new Uint8Array(ctx.data as any), { react: false })
|
|
615
|
-
this.applyRowJson(opts.storeKey, entity_id, existing)
|
|
616
|
-
this.ensureDocumentAwareness(ctx, opts, existing, entity_id)
|
|
562
|
+
applyUpdate(existing.doc, new Uint8Array(ctx.data as any), { react: false })
|
|
563
|
+
this.applyRowJson(opts.storeKey, entity_id, existing.doc)
|
|
617
564
|
return
|
|
618
565
|
}
|
|
619
566
|
|
|
620
|
-
const
|
|
567
|
+
const yjsDoc = new Doc()
|
|
621
568
|
|
|
622
569
|
const update = new Uint8Array(ctx.data as any)
|
|
623
570
|
|
|
@@ -628,10 +575,10 @@ export class CuboCrdtClient<M> {
|
|
|
628
575
|
}
|
|
629
576
|
}
|
|
630
577
|
|
|
631
|
-
applyUpdate(
|
|
578
|
+
applyUpdate(yjsDoc, update, { react: false })
|
|
632
579
|
|
|
633
580
|
// подписываемся на обновления документа
|
|
634
|
-
|
|
581
|
+
yjsDoc.on('update', (update, origin: CuboCrdtClientDocOrigin) => {
|
|
635
582
|
// react = false, если это апдейт с бэка
|
|
636
583
|
if (origin?.react !== false) {
|
|
637
584
|
const data: CuboCrdtServerDocumentIncomingAction = {
|
|
@@ -657,10 +604,11 @@ export class CuboCrdtClient<M> {
|
|
|
657
604
|
})
|
|
658
605
|
|
|
659
606
|
// добавляем документ в хранилище документов
|
|
660
|
-
|
|
607
|
+
const doc: CuboCrdtClientDoc = { doc: yjsDoc, subscribes: new Set() }
|
|
608
|
+
this.docs.set(`${ctx.entity}:${ctx.entity_id}`, doc)
|
|
661
609
|
|
|
662
610
|
// добавляем документ в реактивное хранилище
|
|
663
|
-
const json: any =
|
|
611
|
+
const json: any = yjsDoc.getMap().toJSON()
|
|
664
612
|
const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === entity_id)
|
|
665
613
|
if (index !== undefined && index >= 0) {
|
|
666
614
|
if (this.debug) {
|
|
@@ -673,10 +621,41 @@ export class CuboCrdtClient<M> {
|
|
|
673
621
|
}
|
|
674
622
|
|
|
675
623
|
if (opts?.onAfterCreate) {
|
|
676
|
-
await opts.onAfterCreate(
|
|
624
|
+
await opts.onAfterCreate(yjsDoc, update, ctx, opts)
|
|
677
625
|
}
|
|
678
626
|
|
|
679
|
-
|
|
627
|
+
if (opts?.awareness) {
|
|
628
|
+
let awareness = this.store[opts.storeKey]?.awarenesses.get(entity_id)
|
|
629
|
+
|
|
630
|
+
if (!awareness) {
|
|
631
|
+
awareness = new Awareness(yjsDoc)
|
|
632
|
+
|
|
633
|
+
awareness.on('update', ({ added, updated, removed }, origin) => {
|
|
634
|
+
// console.log('[CRDT] awarness update', origin)
|
|
635
|
+
|
|
636
|
+
if (origin === 'remote') {
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const changed = added.concat(updated).concat(removed)
|
|
641
|
+
const update = encodeAwarenessUpdate(awareness, changed)
|
|
642
|
+
|
|
643
|
+
this.ws.request({
|
|
644
|
+
method: CUBO_CRDT_EVENT.EVENT,
|
|
645
|
+
data: {
|
|
646
|
+
action: 'awareness',
|
|
647
|
+
entity: ctx.entity,
|
|
648
|
+
entity_id,
|
|
649
|
+
data: Array.from(update),
|
|
650
|
+
// 'other' — не шлём свой же курсор обратно исходной подписке
|
|
651
|
+
origin: { expose: 'other', subscribe_id: opts.subscribe_id }
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
this.store[opts.storeKey]?.awarenesses.set(entity_id, awareness)
|
|
657
|
+
}
|
|
658
|
+
}
|
|
680
659
|
}
|
|
681
660
|
|
|
682
661
|
private async onDocumentUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
@@ -685,7 +664,7 @@ export class CuboCrdtClient<M> {
|
|
|
685
664
|
return
|
|
686
665
|
}
|
|
687
666
|
|
|
688
|
-
const doc = this.
|
|
667
|
+
const doc = this.docs.get(`${ctx.entity}:${ctx.entity_id}`)
|
|
689
668
|
if (!doc) {
|
|
690
669
|
console.warn('[CRDT] onDocumentUpdate: doc not exists', { ctx, opts })
|
|
691
670
|
return
|
|
@@ -694,20 +673,20 @@ export class CuboCrdtClient<M> {
|
|
|
694
673
|
const update = new Uint8Array(ctx.data as any)
|
|
695
674
|
|
|
696
675
|
if (opts?.onBeforeUpdate) {
|
|
697
|
-
const result = await opts.onBeforeUpdate(doc, update, ctx, opts)
|
|
676
|
+
const result = await opts.onBeforeUpdate(doc.doc, update, ctx, opts)
|
|
698
677
|
if (!result) {
|
|
699
678
|
return
|
|
700
679
|
}
|
|
701
680
|
}
|
|
702
681
|
|
|
703
682
|
// обновляем yjs-ный документ
|
|
704
|
-
applyUpdate(doc, update, { react: false })
|
|
683
|
+
applyUpdate(doc.doc, update, { react: false })
|
|
705
684
|
|
|
706
685
|
// обновляем документ в реактивном хранилище
|
|
707
|
-
this.applyRowJson(opts.storeKey, entity_id, doc)
|
|
686
|
+
this.applyRowJson(opts.storeKey, entity_id, doc.doc)
|
|
708
687
|
|
|
709
688
|
if (opts?.onAfterUpdate) {
|
|
710
|
-
await opts.onAfterUpdate(doc, update, ctx, opts)
|
|
689
|
+
await opts.onAfterUpdate(doc.doc, update, ctx, opts)
|
|
711
690
|
}
|
|
712
691
|
}
|
|
713
692
|
|
|
@@ -717,23 +696,21 @@ export class CuboCrdtClient<M> {
|
|
|
717
696
|
return
|
|
718
697
|
}
|
|
719
698
|
|
|
720
|
-
const doc = this.
|
|
699
|
+
const doc = this.docs.get(`${ctx.entity}:${ctx.entity_id}`)
|
|
721
700
|
if (!doc) {
|
|
722
701
|
console.warn('[CRDT] onDocumentDelete: doc not exists', { ctx, opts })
|
|
723
702
|
return
|
|
724
703
|
}
|
|
725
704
|
|
|
726
705
|
if (opts?.onBeforeDelete) {
|
|
727
|
-
const result = await opts.onBeforeDelete(doc, ctx, opts)
|
|
706
|
+
const result = await opts.onBeforeDelete(doc.doc, ctx, opts)
|
|
728
707
|
if (!result) {
|
|
729
708
|
return
|
|
730
709
|
}
|
|
731
710
|
}
|
|
732
711
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
doc.destroy()
|
|
736
|
-
this.store[opts.storeKey]?.docs.delete(entity_id)
|
|
712
|
+
doc.doc.destroy()
|
|
713
|
+
this.docs.delete(`${ctx.entity}:${ctx.entity_id}`)
|
|
737
714
|
|
|
738
715
|
const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === entity_id)
|
|
739
716
|
if (index !== undefined && index >= 0) {
|
|
@@ -753,7 +730,7 @@ export class CuboCrdtClient<M> {
|
|
|
753
730
|
}
|
|
754
731
|
|
|
755
732
|
if (opts?.onAfterDelete) {
|
|
756
|
-
await opts.onAfterDelete(doc, ctx, opts)
|
|
733
|
+
await opts.onAfterDelete(doc.doc, ctx, opts)
|
|
757
734
|
}
|
|
758
735
|
}
|
|
759
736
|
|
|
@@ -788,7 +765,9 @@ export class CuboCrdtClient<M> {
|
|
|
788
765
|
const ids = data.ids.filter((id) => Number.isFinite(id))
|
|
789
766
|
const idsSet = new Set(ids)
|
|
790
767
|
|
|
791
|
-
for (const
|
|
768
|
+
for (const row of item.state.rows) {
|
|
769
|
+
const entity_id = (row as any).id
|
|
770
|
+
|
|
792
771
|
if (idsSet.has(entity_id)) {
|
|
793
772
|
continue
|
|
794
773
|
}
|
|
@@ -801,14 +780,15 @@ export class CuboCrdtClient<M> {
|
|
|
801
780
|
item.awarenesses.delete(entity_id)
|
|
802
781
|
}
|
|
803
782
|
|
|
804
|
-
doc.
|
|
805
|
-
|
|
783
|
+
const doc = this.docs.get(`${ctx.entity}:${entity_id}`)
|
|
784
|
+
if (doc) {
|
|
785
|
+
doc.doc.destroy()
|
|
786
|
+
this.docs.delete(`${ctx.entity}:${entity_id}`)
|
|
787
|
+
}
|
|
806
788
|
}
|
|
807
789
|
|
|
808
790
|
const rowsById = new Map(item.state.rows.map((row: any) => [Number(row.id), row]))
|
|
809
|
-
const orderedRows = ids
|
|
810
|
-
.map((id) => rowsById.get(id))
|
|
811
|
-
.filter((row) => row !== undefined)
|
|
791
|
+
const orderedRows = ids.map((id) => rowsById.get(id)).filter((row) => row !== undefined)
|
|
812
792
|
|
|
813
793
|
item.state.rows.splice(0, item.state.rows.length, ...orderedRows)
|
|
814
794
|
item.state.totals = data.totals || {}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { Doc } from 'yjs'
|
|
1
2
|
import { CuboCrdtClientUseOptions } from '..'
|
|
2
3
|
import { CuboCrdtExposeStrategy } from '../../types'
|
|
3
4
|
|
|
4
5
|
import { CuboCrdtClientBaseOptions } from './utils'
|
|
5
6
|
|
|
7
|
+
export type CuboCrdtClientDoc = {
|
|
8
|
+
doc: Doc
|
|
9
|
+
subscribes: Set<string>
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export type CuboCrdtClientDocOrigin = {
|
|
7
13
|
// сохранять ли на бэке в дебаунсе
|
|
8
14
|
store?: boolean
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { type WsClient } from '@cuboapp/ws'
|
|
2
2
|
import { ComputedRef } from 'vue'
|
|
3
3
|
import { Awareness } from 'y-protocols/awareness'
|
|
4
|
-
import { type Doc } from 'yjs'
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
CuboCrdtAction,
|
|
8
|
-
CuboCrdtListSyncData,
|
|
9
|
-
CuboCrdtListTotals,
|
|
10
|
-
CuboCrdtSubscriptionStatus
|
|
11
|
-
} from '../../types'
|
|
5
|
+
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtListTotals, CuboCrdtSubscriptionStatus } from '../../types'
|
|
12
6
|
|
|
7
|
+
import { Doc } from 'yjs'
|
|
13
8
|
import { CuboCrdtClientBaseOptions } from './utils'
|
|
14
9
|
|
|
15
10
|
export * from './document'
|
|
@@ -45,7 +40,6 @@ export type CuboCrdtClientSubscribeEvent = {
|
|
|
45
40
|
export type CuboCrdtClientList<T> = {
|
|
46
41
|
subscribe_id: string
|
|
47
42
|
storeKey: string
|
|
48
|
-
docs: ComputedRef<Map<number, Doc> | undefined>
|
|
49
43
|
awarenesses?: ComputedRef<Map<number, Awareness> | undefined>
|
|
50
44
|
subscribe: (filters?: any) => void
|
|
51
45
|
upgrade: (filters?: any) => void
|
|
@@ -64,7 +58,7 @@ export type CuboCrdtClientList<T> = {
|
|
|
64
58
|
export type CuboCrdtClientRow<T> = {
|
|
65
59
|
subscribe_id: string
|
|
66
60
|
storeKey: string
|
|
67
|
-
doc:
|
|
61
|
+
doc: Doc
|
|
68
62
|
awareness?: ComputedRef<Awareness | undefined>
|
|
69
63
|
subscribe: () => void
|
|
70
64
|
upgrade: (filters?: any) => void
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { Reactive } from 'vue'
|
|
2
|
-
import { Doc } from 'yjs'
|
|
3
2
|
import { Awareness } from 'y-protocols/awareness'
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
CuboCrdtKey,
|
|
7
|
-
CuboCrdtListTotals,
|
|
8
|
-
CuboCrdtSubscriptionStatus
|
|
9
|
-
} from '../../types'
|
|
4
|
+
import { CuboCrdtKey, CuboCrdtListTotals, CuboCrdtSubscriptionStatus } from '../../types'
|
|
10
5
|
|
|
11
6
|
export type CuboCrdtClientStoreItem<T> = {
|
|
12
7
|
state: Reactive<{
|
|
@@ -18,7 +13,6 @@ export type CuboCrdtClientStoreItem<T> = {
|
|
|
18
13
|
totals: CuboCrdtListTotals
|
|
19
14
|
revision: number
|
|
20
15
|
}>
|
|
21
|
-
docs: Map<number, Doc>
|
|
22
16
|
awarenesses: Map<number, Awareness>
|
|
23
17
|
}
|
|
24
18
|
export type CuboCrdtClientStore<M> = Partial<{
|
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import { cloneDeep, pick } from '@cuboapp/utils'
|
|
1
|
+
import { cloneDeep, debounce, pick } from '@cuboapp/utils'
|
|
2
2
|
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'
|
|
3
3
|
import { Awareness, encodeAwarenessUpdate } from 'y-protocols/awareness'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
CuboCrdtServerDocumentOptions,
|
|
7
|
-
CuboCrdtServerDocumentOrigin,
|
|
8
|
-
CuboCrdtServerDocumentPendingStore
|
|
9
|
-
} from '../types'
|
|
10
|
-
import { getCrdtStoreChangeKey } from '../utils'
|
|
11
|
-
|
|
12
|
-
const STORE_DEBOUNCE_MS = 300
|
|
5
|
+
import { CuboCrdtServerDocumentOptions, CuboCrdtServerDocumentOrigin } from '../types'
|
|
13
6
|
|
|
14
7
|
export class CuboCrdtServerDocument {
|
|
15
8
|
private ydoc: Doc
|
|
16
9
|
private persistedState: object = {}
|
|
17
|
-
private observedState: object = {}
|
|
18
|
-
private storeQueue: Promise<void> = Promise.resolve()
|
|
19
|
-
private pendingStore?: CuboCrdtServerDocumentPendingStore
|
|
20
|
-
private storeTimeout?: ReturnType<typeof setTimeout>
|
|
21
10
|
public awareness: Awareness
|
|
22
11
|
public awarenessBySubscribe = new Map<string, Set<number>>()
|
|
23
12
|
|
|
@@ -30,11 +19,31 @@ export class CuboCrdtServerDocument {
|
|
|
30
19
|
})
|
|
31
20
|
|
|
32
21
|
if (opts.awareness) {
|
|
33
|
-
this.
|
|
22
|
+
this.awareness = new Awareness(this.ydoc)
|
|
23
|
+
this.awareness.setLocalState(null)
|
|
24
|
+
|
|
25
|
+
this.awareness.on('update', ({ added, updated, removed }, origin: CuboCrdtServerDocumentOrigin) => {
|
|
26
|
+
const subId = origin?.subscribe_id
|
|
27
|
+
|
|
28
|
+
if (subId) {
|
|
29
|
+
let set = this.awarenessBySubscribe.get(subId)
|
|
30
|
+
if (!set) {
|
|
31
|
+
set = new Set()
|
|
32
|
+
this.awarenessBySubscribe.set(subId, set)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
added.concat(updated).forEach((id) => set!.add(id))
|
|
36
|
+
removed.forEach((id) => set!.delete(id))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const changed = added.concat(updated).concat(removed)
|
|
40
|
+
const update = encodeAwarenessUpdate(this.awareness!, changed)
|
|
41
|
+
|
|
42
|
+
this.opts?.onAwarenessUpdate?.(update, origin)
|
|
43
|
+
})
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
this.persistedState = cloneDeep(opts.initialState ?? {})
|
|
37
|
-
this.observedState = cloneDeep(opts.initialState ?? {})
|
|
38
47
|
|
|
39
48
|
// записываем исходное состояние
|
|
40
49
|
this.write(opts.initialState ?? {})
|
|
@@ -49,8 +58,7 @@ export class CuboCrdtServerDocument {
|
|
|
49
58
|
this.opts?.onUpdate?.(data, origin)
|
|
50
59
|
// }
|
|
51
60
|
|
|
52
|
-
//
|
|
53
|
-
// debounceStore сам обновит observedState после вычисления leaf-diff.
|
|
61
|
+
// если нужно сохранять стейт документа - вызываем store по дебаунсу
|
|
54
62
|
if (origin?.store) {
|
|
55
63
|
const row = this.getJson()
|
|
56
64
|
const body = pick(row, origin.keys ?? Object.keys(row))
|
|
@@ -60,41 +68,10 @@ export class CuboCrdtServerDocument {
|
|
|
60
68
|
console.error('[CRDT] store document', this.name, error)
|
|
61
69
|
})
|
|
62
70
|
}
|
|
63
|
-
} else {
|
|
64
|
-
this.observedState = cloneDeep(this.getJson())
|
|
65
71
|
}
|
|
66
72
|
})
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
public enableAwareness() {
|
|
70
|
-
if (this.awareness) {
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this.awareness = new Awareness(this.ydoc)
|
|
75
|
-
this.awareness.setLocalState(null)
|
|
76
|
-
|
|
77
|
-
this.awareness.on('update', ({ added, updated, removed }, origin: CuboCrdtServerDocumentOrigin) => {
|
|
78
|
-
const subId = origin?.subscribe_id
|
|
79
|
-
|
|
80
|
-
if (subId) {
|
|
81
|
-
let set = this.awarenessBySubscribe.get(subId)
|
|
82
|
-
if (!set) {
|
|
83
|
-
set = new Set()
|
|
84
|
-
this.awarenessBySubscribe.set(subId, set)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
added.concat(updated).forEach((id) => set!.add(id))
|
|
88
|
-
removed.forEach((id) => set!.delete(id))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const changed = added.concat(updated).concat(removed)
|
|
92
|
-
const update = encodeAwarenessUpdate(this.awareness!, changed)
|
|
93
|
-
|
|
94
|
-
this.opts?.onAwarenessUpdate?.(update, origin)
|
|
95
|
-
})
|
|
96
|
-
}
|
|
97
|
-
|
|
98
75
|
public get name() {
|
|
99
76
|
return this.opts.name
|
|
100
77
|
}
|
|
@@ -152,117 +129,17 @@ export class CuboCrdtServerDocument {
|
|
|
152
129
|
}
|
|
153
130
|
|
|
154
131
|
destroy() {
|
|
155
|
-
this.flushPendingStore()
|
|
156
132
|
this.awareness?.destroy()
|
|
157
133
|
this.ydoc.destroy()
|
|
158
134
|
}
|
|
159
135
|
|
|
160
|
-
public store(body: object, origin: CuboCrdtServerDocumentOrigin
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
// debounce ограничивает частоту вызовов, но не сериализует async store.
|
|
165
|
-
// Без очереди следующий PATCH мог завершиться раньше предыдущего и затем
|
|
166
|
-
// быть затёрт старым полным снимком составного поля (например case.extra).
|
|
167
|
-
const current = this.storeQueue
|
|
168
|
-
.catch(() => undefined)
|
|
169
|
-
.then(async () => {
|
|
170
|
-
const previousRow = this.getPersistedState()
|
|
171
|
-
|
|
172
|
-
await this.opts?.onStore?.(queuedBody, origin, {
|
|
173
|
-
previousRow,
|
|
174
|
-
row: queuedRow
|
|
175
|
-
})
|
|
176
|
-
this.setPersistedState(queuedRow)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
this.storeQueue = current
|
|
180
|
-
return current
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Схлопывает только соседние изменения одного origin и одного набора
|
|
185
|
-
* leaf-путей. Смена пользователя или поля немедленно фиксирует предыдущий
|
|
186
|
-
* пакет, сохраняя фактический порядок collaborative-обновлений.
|
|
187
|
-
*/
|
|
188
|
-
public debounceStore(body: object, origin: CuboCrdtServerDocumentOrigin) {
|
|
189
|
-
const row = cloneDeep(this.getJson())
|
|
190
|
-
const bodyKeys = Object.keys(body)
|
|
191
|
-
const previousBody = pick(this.observedState, bodyKeys)
|
|
192
|
-
const changeKey = getCrdtStoreChangeKey(previousBody, body)
|
|
193
|
-
const originKey = `${origin.client_id || ''}:${origin.subscribe_id || ''}`
|
|
194
|
-
|
|
195
|
-
this.observedState = cloneDeep(row)
|
|
196
|
-
|
|
197
|
-
if (!changeKey) {
|
|
198
|
-
return Promise.resolve()
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return new Promise<void>((resolve, reject) => {
|
|
202
|
-
const sameBatch = this.pendingStore?.originKey === originKey &&
|
|
203
|
-
this.pendingStore.changeKey === changeKey
|
|
204
|
-
|
|
205
|
-
if (this.pendingStore && !sameBatch) {
|
|
206
|
-
this.flushPendingStore()
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (!this.pendingStore) {
|
|
210
|
-
this.pendingStore = {
|
|
211
|
-
body: cloneDeep(body),
|
|
212
|
-
changeKey,
|
|
213
|
-
origin: cloneDeep(origin),
|
|
214
|
-
originKey,
|
|
215
|
-
row,
|
|
216
|
-
waiters: []
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
const keys = new Set([
|
|
220
|
-
...(this.pendingStore.origin.keys || []),
|
|
221
|
-
...(origin.keys || [])
|
|
222
|
-
])
|
|
223
|
-
|
|
224
|
-
this.pendingStore.body = {
|
|
225
|
-
...this.pendingStore.body,
|
|
226
|
-
...cloneDeep(body)
|
|
227
|
-
}
|
|
228
|
-
this.pendingStore.origin = {
|
|
229
|
-
...cloneDeep(origin),
|
|
230
|
-
keys: [...keys]
|
|
231
|
-
}
|
|
232
|
-
this.pendingStore.row = row
|
|
233
|
-
}
|
|
136
|
+
public async store(body: object, origin: CuboCrdtServerDocumentOrigin) {
|
|
137
|
+
const previousRow = this.getPersistedState()
|
|
138
|
+
const row = this.getJson()
|
|
234
139
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
})
|
|
140
|
+
await this.opts?.onStore?.(body, origin, { previousRow, row })
|
|
141
|
+
this.setPersistedState(row)
|
|
238
142
|
}
|
|
239
143
|
|
|
240
|
-
|
|
241
|
-
private schedulePendingStore() {
|
|
242
|
-
if (this.storeTimeout) {
|
|
243
|
-
clearTimeout(this.storeTimeout)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
this.storeTimeout = setTimeout(() => this.flushPendingStore(), STORE_DEBOUNCE_MS)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Передаёт накопленный пакет в общую последовательную очередь записи. */
|
|
250
|
-
private flushPendingStore() {
|
|
251
|
-
if (!this.pendingStore) {
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (this.storeTimeout) {
|
|
256
|
-
clearTimeout(this.storeTimeout)
|
|
257
|
-
this.storeTimeout = undefined
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const pending = this.pendingStore
|
|
261
|
-
this.pendingStore = undefined
|
|
262
|
-
|
|
263
|
-
this.store(pending.body, pending.origin, pending.row).then(
|
|
264
|
-
() => pending.waiters.forEach(({ resolve }) => resolve()),
|
|
265
|
-
(error) => pending.waiters.forEach(({ reject }) => reject(error))
|
|
266
|
-
)
|
|
267
|
-
}
|
|
144
|
+
public debounceStore = debounce(this.store.bind(this), 300)
|
|
268
145
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -4,11 +4,7 @@ import { applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } fr
|
|
|
4
4
|
|
|
5
5
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
6
6
|
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtMutation } from '../types'
|
|
7
|
-
import {
|
|
8
|
-
areCrdtListIdsEqual,
|
|
9
|
-
areCrdtListTotalsEqual,
|
|
10
|
-
checkRowIsSutable
|
|
11
|
-
} from '../utils'
|
|
7
|
+
import { areCrdtListIdsEqual, areCrdtListTotalsEqual, checkRowIsSutable } from '../utils'
|
|
12
8
|
|
|
13
9
|
import { CuboCrdtClientDocOrigin } from '../client'
|
|
14
10
|
import { CuboCrdtServerDocument } from './document'
|
|
@@ -437,12 +433,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
437
433
|
const generation = this.subscribeGenerations.get(subscribe_id) || 0
|
|
438
434
|
const forceSync = state.forceSync
|
|
439
435
|
state.forceSync = false
|
|
440
|
-
const reconciled = await this.reconcileSubscribe(
|
|
441
|
-
client,
|
|
442
|
-
subscribe,
|
|
443
|
-
generation,
|
|
444
|
-
forceSync
|
|
445
|
-
)
|
|
436
|
+
const reconciled = await this.reconcileSubscribe(client, subscribe, generation, forceSync)
|
|
446
437
|
if (!reconciled && forceSync) {
|
|
447
438
|
state.forceSync = true
|
|
448
439
|
}
|
|
@@ -454,11 +445,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
454
445
|
return state.running
|
|
455
446
|
}
|
|
456
447
|
|
|
457
|
-
private requestSubscribeRefresh(
|
|
458
|
-
subscribe_id: string,
|
|
459
|
-
immediate = false,
|
|
460
|
-
forceSync = immediate
|
|
461
|
-
) {
|
|
448
|
+
private requestSubscribeRefresh(subscribe_id: string, immediate = false, forceSync = immediate) {
|
|
462
449
|
const state = this.getSubscribeRefreshState(subscribe_id)
|
|
463
450
|
state.dirty = true
|
|
464
451
|
state.forceSync ||= forceSync
|
|
@@ -544,16 +531,8 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
544
531
|
}
|
|
545
532
|
|
|
546
533
|
if (mutation.previousRow) {
|
|
547
|
-
const previousSuitable = this.checkRowIsSutable(
|
|
548
|
-
|
|
549
|
-
subscribe,
|
|
550
|
-
entity
|
|
551
|
-
)
|
|
552
|
-
const currentSuitable = this.checkRowIsSutable(
|
|
553
|
-
mutation.row,
|
|
554
|
-
subscribe,
|
|
555
|
-
entity
|
|
556
|
-
)
|
|
534
|
+
const previousSuitable = this.checkRowIsSutable(mutation.previousRow, subscribe, entity)
|
|
535
|
+
const currentSuitable = this.checkRowIsSutable(mutation.row, subscribe, entity)
|
|
557
536
|
|
|
558
537
|
if (previousSuitable !== currentSuitable) {
|
|
559
538
|
return true
|
|
@@ -605,10 +584,6 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
605
584
|
|
|
606
585
|
let document = this.documents.get(documentName)
|
|
607
586
|
if (document) {
|
|
608
|
-
if (awareness) {
|
|
609
|
-
document.enableAwareness()
|
|
610
|
-
}
|
|
611
|
-
|
|
612
587
|
// Освежаем существующий документ строкой из БД.
|
|
613
588
|
//
|
|
614
589
|
// Раньше документ возвращался как есть, а подписка отправляла клиенту его
|
|
@@ -840,13 +815,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
840
815
|
|
|
841
816
|
const subscribeSutable = this.checkSubscribeIsSutable(origin, subscribe)
|
|
842
817
|
const documentExistsInSubscribe = this.subscribesByDocument.get(document.name)?.has(subscribe.id)
|
|
843
|
-
const rowSutable = subscribe.paginated
|
|
844
|
-
? documentExistsInSubscribe
|
|
845
|
-
: this.checkRowIsSutable(row, subscribe, entity)
|
|
818
|
+
const rowSutable = subscribe.paginated ? documentExistsInSubscribe : this.checkRowIsSutable(row, subscribe, entity)
|
|
846
819
|
|
|
847
|
-
const sutable = subscribeSutable && (subscribe.paginated
|
|
848
|
-
? documentExistsInSubscribe
|
|
849
|
-
: rowSutable || documentExistsInSubscribe)
|
|
820
|
+
const sutable = subscribeSutable && (subscribe.paginated ? documentExistsInSubscribe : rowSutable || documentExistsInSubscribe)
|
|
850
821
|
|
|
851
822
|
if (sutable) {
|
|
852
823
|
if (this.debug && !rowSutable) {
|
|
@@ -947,21 +918,13 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
947
918
|
}
|
|
948
919
|
}
|
|
949
920
|
|
|
950
|
-
private async reconcileSubscribe(
|
|
951
|
-
client: WsServerSocket,
|
|
952
|
-
subscribe: CuboCrdtServerSubscribe,
|
|
953
|
-
generation: number,
|
|
954
|
-
forceSync: boolean
|
|
955
|
-
) {
|
|
921
|
+
private async reconcileSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe, generation: number, forceSync: boolean) {
|
|
956
922
|
const fetched = await this.options.fetchRows?.(client, subscribe)
|
|
957
923
|
const result = Array.isArray(fetched)
|
|
958
924
|
? { rows: fetched, totals: subscribe.totals }
|
|
959
925
|
: { rows: fetched?.rows || [], totals: fetched?.totals || subscribe.totals }
|
|
960
926
|
|
|
961
|
-
if (
|
|
962
|
-
this.subscribes.get(subscribe.id) !== subscribe ||
|
|
963
|
-
this.subscribeGenerations.get(subscribe.id) !== generation
|
|
964
|
-
) {
|
|
927
|
+
if (this.subscribes.get(subscribe.id) !== subscribe || this.subscribeGenerations.get(subscribe.id) !== generation) {
|
|
965
928
|
return false
|
|
966
929
|
}
|
|
967
930
|
|
|
@@ -1034,10 +997,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1034
997
|
}
|
|
1035
998
|
|
|
1036
999
|
const idsChanged = !areCrdtListIdsEqual(subscribe.row_ids, nextIds)
|
|
1037
|
-
const totalsChanged = !areCrdtListTotalsEqual(
|
|
1038
|
-
subscribe.totals,
|
|
1039
|
-
result.totals
|
|
1040
|
-
)
|
|
1000
|
+
const totalsChanged = !areCrdtListTotalsEqual(subscribe.totals, result.totals)
|
|
1041
1001
|
|
|
1042
1002
|
subscribe.row_ids = nextIds
|
|
1043
1003
|
subscribe.totals = result.totals
|
|
@@ -1076,10 +1036,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1076
1036
|
if (upgrade.filters !== undefined) {
|
|
1077
1037
|
subscribe.filters = cloneDeep(upgrade.filters || {})
|
|
1078
1038
|
}
|
|
1079
|
-
subscribe.paginated = this.resolvePaginated(
|
|
1080
|
-
subscribe.filters,
|
|
1081
|
-
upgrade.paginated
|
|
1082
|
-
)
|
|
1039
|
+
subscribe.paginated = this.resolvePaginated(subscribe.filters, upgrade.paginated)
|
|
1083
1040
|
|
|
1084
1041
|
return this.requestSubscribeRefresh(subscribe.id, true)
|
|
1085
1042
|
}
|
|
@@ -5,20 +5,6 @@ export type CuboCrdtServerDocumentStoreContext = {
|
|
|
5
5
|
row: object
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export type CuboCrdtServerDocumentStoreWaiter = {
|
|
9
|
-
resolve: () => void
|
|
10
|
-
reject: (reason?: unknown) => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type CuboCrdtServerDocumentPendingStore = {
|
|
14
|
-
body: object
|
|
15
|
-
changeKey: string
|
|
16
|
-
origin: CuboCrdtServerDocumentOrigin
|
|
17
|
-
originKey: string
|
|
18
|
-
row: object
|
|
19
|
-
waiters: CuboCrdtServerDocumentStoreWaiter[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
8
|
export type CuboCrdtServerDocumentOptions = {
|
|
23
9
|
name: string
|
|
24
10
|
onStore?: (
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
const isObject = (value: unknown): value is Record<string, unknown> => {
|
|
2
|
-
return value !== null && typeof value === 'object'
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
const collectChangedPaths = (
|
|
6
|
-
previous: unknown,
|
|
7
|
-
current: unknown,
|
|
8
|
-
path: string,
|
|
9
|
-
result: string[]
|
|
10
|
-
) => {
|
|
11
|
-
if (Object.is(previous, current)) {
|
|
12
|
-
return
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (!isObject(previous) || !isObject(current)) {
|
|
16
|
-
result.push(path || '$')
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const keys = new Set([
|
|
21
|
-
...Object.keys(previous),
|
|
22
|
-
...Object.keys(current)
|
|
23
|
-
])
|
|
24
|
-
|
|
25
|
-
for (const key of [...keys].sort()) {
|
|
26
|
-
const nextPath = Array.isArray(previous) || Array.isArray(current)
|
|
27
|
-
? `${path}[${key}]`
|
|
28
|
-
: path ? `${path}.${key}` : key
|
|
29
|
-
|
|
30
|
-
collectChangedPaths(previous[key], current[key], nextPath, result)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Возвращает стабильный ключ фактически изменённых leaf-путей объекта. */
|
|
35
|
-
export const getCrdtStoreChangeKey = (previous: object, current: object) => {
|
|
36
|
-
const paths: string[] = []
|
|
37
|
-
|
|
38
|
-
collectChangedPaths(previous, current, '', paths)
|
|
39
|
-
|
|
40
|
-
return paths.join('|')
|
|
41
|
-
}
|