@cuboapp/crdt 1.0.23 → 1.0.25
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 +175 -232
- package/src/client/types/document.ts +13 -0
- package/src/client/types/index.ts +1 -10
- package/src/client/types/store.ts +1 -9
- package/src/server/index.ts +45 -50
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
|
-
import { applyUpdate, Doc
|
|
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,9 @@ export * from './types'
|
|
|
28
25
|
|
|
29
26
|
export class CuboCrdtClient<M> {
|
|
30
27
|
public store: CuboCrdtClientStore<M> = {}
|
|
28
|
+
// Реактивен только реестр: Vue отслеживает set/delete и повторно вычисляет
|
|
29
|
+
// computed(useDoc). Сами Y.Doc/Awareness остаются исходными объектами без proxy.
|
|
30
|
+
public docs = new Map<string, CuboCrdtClientDoc>()
|
|
31
31
|
|
|
32
32
|
private listeners: Map<string, Map<string, (ctx: CuboCrdtClientSubscribeEvent) => void | Promise<void>>> = new Map()
|
|
33
33
|
|
|
@@ -110,6 +110,11 @@ export class CuboCrdtClient<M> {
|
|
|
110
110
|
|
|
111
111
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
|
|
112
112
|
|
|
113
|
+
for (const document of this.docs.values()) {
|
|
114
|
+
document.awareness?.destroy()
|
|
115
|
+
document.doc.destroy()
|
|
116
|
+
}
|
|
117
|
+
this.docs.clear()
|
|
113
118
|
this.listeners.clear()
|
|
114
119
|
this.subscriptions.clear()
|
|
115
120
|
}
|
|
@@ -233,9 +238,7 @@ export class CuboCrdtClient<M> {
|
|
|
233
238
|
})
|
|
234
239
|
|
|
235
240
|
this.store[storeKey] = {
|
|
236
|
-
state
|
|
237
|
-
docs: shallowReactive(new Map()),
|
|
238
|
-
awarenesses: shallowReactive(new Map())
|
|
241
|
+
state
|
|
239
242
|
}
|
|
240
243
|
}
|
|
241
244
|
|
|
@@ -260,6 +263,7 @@ export class CuboCrdtClient<M> {
|
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
// подписываемся на фронте
|
|
266
|
+
// console.log('subscribe', opts)
|
|
263
267
|
this.listeners.get(entity)?.set(subscribe_id, async (ctx) => {
|
|
264
268
|
await this.onIncomingUpdate(ctx, {
|
|
265
269
|
subscribe_id,
|
|
@@ -330,13 +334,11 @@ export class CuboCrdtClient<M> {
|
|
|
330
334
|
|
|
331
335
|
// отписываемся на бэке
|
|
332
336
|
if (this.ws.connected) {
|
|
333
|
-
void this.ws
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
})
|
|
337
|
+
void this.ws.request({ method: CUBO_CRDT_EVENT.UNSUBSCRIBE, data: { subscribe_id } }).catch((e) => {
|
|
338
|
+
if (this.debug) {
|
|
339
|
+
console.warn('[CRDT] unsubscribe request failed (socket down)', subscribe_id, e)
|
|
340
|
+
}
|
|
341
|
+
})
|
|
340
342
|
}
|
|
341
343
|
|
|
342
344
|
// отписываемся на фронте
|
|
@@ -347,21 +349,13 @@ export class CuboCrdtClient<M> {
|
|
|
347
349
|
this.store[storeKey]!.state.status = 'idle'
|
|
348
350
|
this.store[storeKey]!.state.error = undefined
|
|
349
351
|
|
|
352
|
+
const item = this.store[storeKey]
|
|
353
|
+
item?.state.rows.forEach((row) => {
|
|
354
|
+
this.detachDocument(`${entity}`, (row as any).id, subscribe_id, storeKey)
|
|
355
|
+
})
|
|
356
|
+
|
|
350
357
|
// очищаем стор
|
|
351
358
|
if (clear !== false) {
|
|
352
|
-
const item = this.store[storeKey]
|
|
353
|
-
if (item) {
|
|
354
|
-
item.awarenesses?.forEach((a) => {
|
|
355
|
-
removeAwarenessStates(a, [a.doc.clientID], 'unsubscribe')
|
|
356
|
-
|
|
357
|
-
a.setLocalState(null)
|
|
358
|
-
a.destroy()
|
|
359
|
-
})
|
|
360
|
-
item.awarenesses?.clear()
|
|
361
|
-
item.docs?.forEach((d) => d.destroy())
|
|
362
|
-
item.docs?.clear()
|
|
363
|
-
}
|
|
364
|
-
|
|
365
359
|
this.store[storeKey] = undefined
|
|
366
360
|
}
|
|
367
361
|
}
|
|
@@ -383,8 +377,6 @@ export class CuboCrdtClient<M> {
|
|
|
383
377
|
subscribe,
|
|
384
378
|
upgrade,
|
|
385
379
|
unsubscribe,
|
|
386
|
-
docs: computed(() => this.store[storeKey]?.docs),
|
|
387
|
-
awarenesses: opts?.awareness ? computed(() => this.store[storeKey]?.awarenesses) : undefined,
|
|
388
380
|
subscribed: () => computed(() => this.store[storeKey]?.state.subscribed || false),
|
|
389
381
|
loading: () => computed(() => this.store[storeKey]?.state.loading || false),
|
|
390
382
|
ready: () => computed(() => this.store[storeKey]?.state.status === 'ready'),
|
|
@@ -397,6 +389,10 @@ export class CuboCrdtClient<M> {
|
|
|
397
389
|
}
|
|
398
390
|
}
|
|
399
391
|
|
|
392
|
+
public useDoc<K extends Extract<keyof M, string>>(entity: K, id: number) {
|
|
393
|
+
return this.docs.get(`${entity}:${id}`)
|
|
394
|
+
}
|
|
395
|
+
|
|
400
396
|
public useRow<K extends Extract<keyof M, string>, T = CuboCrdtKey<K, M>>(
|
|
401
397
|
entity: K | string,
|
|
402
398
|
id: number,
|
|
@@ -404,20 +400,7 @@ export class CuboCrdtClient<M> {
|
|
|
404
400
|
): CuboCrdtClientRow<T> {
|
|
405
401
|
// console.log('store key', opts?.storeKey ?? `${entity}:${id}`)
|
|
406
402
|
|
|
407
|
-
const {
|
|
408
|
-
subscribe_id,
|
|
409
|
-
storeKey,
|
|
410
|
-
docs,
|
|
411
|
-
awarenesses,
|
|
412
|
-
subscribe,
|
|
413
|
-
upgrade,
|
|
414
|
-
unsubscribe,
|
|
415
|
-
subscribed,
|
|
416
|
-
loading,
|
|
417
|
-
ready,
|
|
418
|
-
status,
|
|
419
|
-
error
|
|
420
|
-
} = this.useList(entity, {
|
|
403
|
+
const { subscribe_id, storeKey, subscribe, upgrade, unsubscribe, subscribed, loading, ready, status, error } = this.useList(entity, {
|
|
421
404
|
...opts,
|
|
422
405
|
filters: opts?.filters ?? { id },
|
|
423
406
|
storeKey: opts?.storeKey ?? `${entity}:${id}`
|
|
@@ -426,8 +409,6 @@ export class CuboCrdtClient<M> {
|
|
|
426
409
|
return {
|
|
427
410
|
subscribe_id,
|
|
428
411
|
storeKey,
|
|
429
|
-
doc: computed(() => docs.value?.get(id)),
|
|
430
|
-
awareness: opts?.awareness ? computed(() => awarenesses?.value?.get(id)) : undefined,
|
|
431
412
|
subscribe,
|
|
432
413
|
upgrade,
|
|
433
414
|
unsubscribe,
|
|
@@ -450,92 +431,30 @@ export class CuboCrdtClient<M> {
|
|
|
450
431
|
dto: K extends Extract<keyof M, string> ? Partial<M[K]> : object,
|
|
451
432
|
opts?: CuboCrdtClientDocOrigin
|
|
452
433
|
) {
|
|
453
|
-
|
|
454
|
-
// каноническим документом. Списки могут содержать тот же id, но не должны
|
|
455
|
-
// становиться источником записи для карточки.
|
|
456
|
-
const rowStore = this.store[`${entity}:${entity_id}`]
|
|
457
|
-
const store = rowStore?.state.status === 'ready' && rowStore.docs.has(entity_id)
|
|
458
|
-
? rowStore
|
|
459
|
-
: [...this.subscriptions.values()]
|
|
460
|
-
.filter((subscription) => subscription.entity === entity)
|
|
461
|
-
.map((subscription) => this.store[subscription.storeKey])
|
|
462
|
-
.find((candidate) => candidate?.docs.has(entity_id) && candidate.state.status === 'ready')
|
|
463
|
-
const document = store?.docs.get(entity_id)
|
|
464
|
-
|
|
465
|
-
if (!store || !document) {
|
|
466
|
-
return false
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const map = document.getMap()
|
|
470
|
-
const toUpdate = Object.fromEntries(
|
|
471
|
-
Object.entries(dto).filter(([key, value]) => map.get(key) !== value)
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
if (!Object.keys(toUpdate).length) {
|
|
475
|
-
// Состояние уже совпадает (например, update успел прийти из другой
|
|
476
|
-
// вкладки). Это успешный no-op, а не ошибка изменения.
|
|
477
|
-
return true
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const origin: CuboCrdtClientDocOrigin = {
|
|
481
|
-
store: opts?.store ?? true,
|
|
482
|
-
expose: opts?.expose ?? 'other',
|
|
483
|
-
keys: opts?.keys ?? Object.keys(toUpdate),
|
|
484
|
-
// update() сам отправляет ровно один пакет ниже. Общий listener остаётся
|
|
485
|
-
// для изменений, которые создаются напрямую сторонними Yjs-binding'ами.
|
|
486
|
-
react: false
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const stateVector = encodeStateVector(document)
|
|
490
|
-
|
|
491
|
-
// Одна сущность может присутствовать в нескольких подписках с разными
|
|
492
|
-
// storeKey. Мутацию отправляем только из одного документа: остальные
|
|
493
|
-
// подписки получат подтверждённый update через обычную CRDT-рассылку.
|
|
494
|
-
document.transact(() => {
|
|
495
|
-
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
496
|
-
map.set(key, value)
|
|
497
|
-
})
|
|
498
|
-
}, origin)
|
|
434
|
+
const storeKey = `${entity}:${entity_id}`
|
|
499
435
|
|
|
500
|
-
const
|
|
501
|
-
const receipt = this.ws
|
|
502
|
-
.request(
|
|
503
|
-
{
|
|
504
|
-
method: CUBO_CRDT_EVENT.EVENT,
|
|
505
|
-
data: {
|
|
506
|
-
action: 'update',
|
|
507
|
-
entity,
|
|
508
|
-
entity_id,
|
|
509
|
-
data: Array.from(update),
|
|
510
|
-
origin: {
|
|
511
|
-
...pick(origin, ['store', 'keys', 'expose']),
|
|
512
|
-
subscribe_id: [...this.subscriptions.entries()]
|
|
513
|
-
.find(([, subscription]) => this.store[subscription.storeKey] === store)?.[0]
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
},
|
|
517
|
-
{ wait: true, timeout: 10_000 }
|
|
518
|
-
)
|
|
519
|
-
.then(() => true)
|
|
520
|
-
.catch((error) => {
|
|
521
|
-
console.error('[CRDT] update request failed', error)
|
|
522
|
-
return false
|
|
523
|
-
})
|
|
436
|
+
const document = this.docs.get(storeKey)
|
|
524
437
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
this.applyRowJson(`${entity}:${entity_id}`, entity_id, document)
|
|
438
|
+
if (document) {
|
|
439
|
+
const map = document.doc.getMap()
|
|
440
|
+
const toUpdate = Object.fromEntries(Object.entries(dto).filter(([key, value]) => map.get(key) !== value))
|
|
529
441
|
|
|
530
|
-
|
|
442
|
+
if (Object.keys(toUpdate).length) {
|
|
443
|
+
const origin: CuboCrdtClientDocOrigin = {
|
|
444
|
+
store: opts?.store ?? true,
|
|
445
|
+
expose: opts?.expose ?? 'other',
|
|
446
|
+
keys: opts?.keys ?? Object.keys(toUpdate),
|
|
447
|
+
react: true,
|
|
448
|
+
subscribe_id: opts?.subscribe_id ?? document.subscribes.values().next().value
|
|
449
|
+
}
|
|
531
450
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
451
|
+
document.doc.transact(() => {
|
|
452
|
+
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
453
|
+
map.set(key, value)
|
|
454
|
+
})
|
|
455
|
+
}, origin)
|
|
456
|
+
}
|
|
536
457
|
}
|
|
537
|
-
|
|
538
|
-
return receipt
|
|
539
458
|
}
|
|
540
459
|
|
|
541
460
|
public upgrade(subscribe_id: string, filters?: Record<string, any>) {
|
|
@@ -579,61 +498,94 @@ export class CuboCrdtClient<M> {
|
|
|
579
498
|
})
|
|
580
499
|
}
|
|
581
500
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const index = rows.findIndex((r: any) => r.id === entity_id)
|
|
591
|
-
if (index >= 0) {
|
|
592
|
-
rows[index] = json
|
|
593
|
-
} else {
|
|
594
|
-
rows.push(json)
|
|
501
|
+
private applyDocumentRows(entity_id: number, document: CuboCrdtClientDoc) {
|
|
502
|
+
for (const subscribe_id of document.subscribes) {
|
|
503
|
+
const subscription = this.subscriptions.get(subscribe_id)
|
|
504
|
+
if (subscription) {
|
|
505
|
+
this.applyRowJson(subscription.storeKey, entity_id, document.doc)
|
|
506
|
+
}
|
|
595
507
|
}
|
|
596
508
|
}
|
|
597
509
|
|
|
598
|
-
private
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
doc: Doc,
|
|
602
|
-
entity_id: number
|
|
603
|
-
) {
|
|
510
|
+
private attachDocument(entity: string, entity_id: number, document: CuboCrdtClientDoc, opts: CuboCrdtClientDocUpdateOptions) {
|
|
511
|
+
document.subscribes.add(opts.subscribe_id)
|
|
512
|
+
|
|
604
513
|
if (!opts.awareness) {
|
|
605
514
|
return
|
|
606
515
|
}
|
|
607
516
|
|
|
608
|
-
|
|
517
|
+
document.awarenessSubscribes.add(opts.subscribe_id)
|
|
609
518
|
|
|
610
|
-
if (awareness) {
|
|
519
|
+
if (!document.awareness) {
|
|
520
|
+
const awareness = new Awareness(document.doc)
|
|
521
|
+
|
|
522
|
+
awareness.on('update', ({ added, updated, removed }, origin) => {
|
|
523
|
+
if (origin === 'remote') {
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const subscribe_id = document.awarenessSubscribes.values().next().value
|
|
528
|
+
if (!subscribe_id) {
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const changed = added.concat(updated).concat(removed)
|
|
533
|
+
const update = encodeAwarenessUpdate(awareness, changed)
|
|
534
|
+
|
|
535
|
+
void this.ws
|
|
536
|
+
.request({
|
|
537
|
+
method: CUBO_CRDT_EVENT.EVENT,
|
|
538
|
+
data: {
|
|
539
|
+
action: 'awareness',
|
|
540
|
+
entity,
|
|
541
|
+
entity_id,
|
|
542
|
+
data: Array.from(update),
|
|
543
|
+
origin: { expose: 'other', subscribe_id }
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
.catch((error) => console.error('[CRDT] awareness request failed', error))
|
|
547
|
+
})
|
|
548
|
+
document.awareness = awareness
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private detachDocument(entity: string, entity_id: number, subscribe_id: string, storeKey: string) {
|
|
553
|
+
const key = `${entity}:${entity_id}`
|
|
554
|
+
const document = this.docs.get(key)
|
|
555
|
+
if (!document) {
|
|
611
556
|
return
|
|
612
557
|
}
|
|
613
558
|
|
|
614
|
-
|
|
559
|
+
document.subscribes.delete(subscribe_id)
|
|
560
|
+
document.awarenessSubscribes.delete(subscribe_id)
|
|
615
561
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
562
|
+
if (!document.awarenessSubscribes.size && document.awareness) {
|
|
563
|
+
removeAwarenessStates(document.awareness, [document.awareness.doc.clientID], 'unsubscribe')
|
|
564
|
+
document.awareness.setLocalState(null)
|
|
565
|
+
document.awareness.destroy()
|
|
566
|
+
document.awareness = undefined
|
|
567
|
+
}
|
|
620
568
|
|
|
621
|
-
|
|
622
|
-
|
|
569
|
+
if (!document.subscribes.size) {
|
|
570
|
+
document.doc.destroy()
|
|
571
|
+
this.docs.delete(key)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
623
574
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
origin: { expose: 'other', subscribe_id: opts.subscribe_id }
|
|
632
|
-
}
|
|
633
|
-
})
|
|
634
|
-
})
|
|
575
|
+
// апсертит строку в реактивное хранилище из текущего состояния yjs-документа
|
|
576
|
+
private applyRowJson(storeKey: string, entity_id: number, doc: Doc) {
|
|
577
|
+
const json: any = doc.getMap().toJSON()
|
|
578
|
+
const rows = this.store[storeKey]?.state.rows
|
|
579
|
+
if (!rows) {
|
|
580
|
+
return
|
|
581
|
+
}
|
|
635
582
|
|
|
636
|
-
|
|
583
|
+
const index = rows.findIndex((r: any) => r.id === entity_id)
|
|
584
|
+
if (index >= 0) {
|
|
585
|
+
rows[index] = json
|
|
586
|
+
} else {
|
|
587
|
+
rows.push(json)
|
|
588
|
+
}
|
|
637
589
|
}
|
|
638
590
|
|
|
639
591
|
private async onDocumentCreate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
@@ -642,7 +594,7 @@ export class CuboCrdtClient<M> {
|
|
|
642
594
|
return
|
|
643
595
|
}
|
|
644
596
|
|
|
645
|
-
const existing = this.
|
|
597
|
+
const existing = this.docs.get(`${ctx.entity}:${ctx.entity_id}`)
|
|
646
598
|
if (existing) {
|
|
647
599
|
// документ уже есть локально (типичный кейс — повторный SUBSCRIBE после реконнекта).
|
|
648
600
|
// вместо игнорирования вмёрживаем входящее состояние в существующий yjs-документ
|
|
@@ -651,13 +603,13 @@ export class CuboCrdtClient<M> {
|
|
|
651
603
|
console.log('[CRDT] onDocumentCreate: doc already exists, merging state', { ctx, opts })
|
|
652
604
|
}
|
|
653
605
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
this.
|
|
606
|
+
this.attachDocument(ctx.entity, entity_id, existing, opts)
|
|
607
|
+
applyUpdate(existing.doc, new Uint8Array(ctx.data as any), { react: false })
|
|
608
|
+
this.applyRowJson(opts.storeKey, entity_id, existing.doc)
|
|
657
609
|
return
|
|
658
610
|
}
|
|
659
611
|
|
|
660
|
-
const
|
|
612
|
+
const yjsDoc = new Doc()
|
|
661
613
|
|
|
662
614
|
const update = new Uint8Array(ctx.data as any)
|
|
663
615
|
|
|
@@ -668,10 +620,22 @@ export class CuboCrdtClient<M> {
|
|
|
668
620
|
}
|
|
669
621
|
}
|
|
670
622
|
|
|
671
|
-
applyUpdate(
|
|
623
|
+
applyUpdate(yjsDoc, update, { react: false })
|
|
624
|
+
|
|
625
|
+
const document: CuboCrdtClientDoc = {
|
|
626
|
+
doc: yjsDoc,
|
|
627
|
+
subscribes: new Set(),
|
|
628
|
+
awarenessSubscribes: new Set()
|
|
629
|
+
}
|
|
630
|
+
this.attachDocument(ctx.entity, entity_id, document, opts)
|
|
631
|
+
// Публикуем контейнер реактивному реестру только после полной инициализации.
|
|
632
|
+
// Тогда первый computed(useDoc) не увидит промежуточный doc без Awareness.
|
|
633
|
+
this.docs.set(`${ctx.entity}:${ctx.entity_id}`, document)
|
|
672
634
|
|
|
673
635
|
// подписываемся на обновления документа
|
|
674
|
-
|
|
636
|
+
yjsDoc.on('update', (update, origin: CuboCrdtClientDocOrigin) => {
|
|
637
|
+
this.applyDocumentRows(entity_id, document)
|
|
638
|
+
|
|
675
639
|
// react = false, если это апдейт с бэка
|
|
676
640
|
if (origin?.react !== false) {
|
|
677
641
|
const data: CuboCrdtServerDocumentIncomingAction = {
|
|
@@ -685,22 +649,21 @@ export class CuboCrdtClient<M> {
|
|
|
685
649
|
// приходят без expose и сервер эхом шлёт их обратно самому автору.
|
|
686
650
|
expose: 'other',
|
|
687
651
|
...pick(origin || {}, ['store', 'keys', 'expose']),
|
|
688
|
-
subscribe_id:
|
|
652
|
+
subscribe_id: origin?.subscribe_id ?? document.subscribes.values().next().value
|
|
689
653
|
}
|
|
690
654
|
}
|
|
691
655
|
|
|
692
|
-
void this.ws
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
656
|
+
void this.ws
|
|
657
|
+
.request({
|
|
658
|
+
method: CUBO_CRDT_EVENT.EVENT,
|
|
659
|
+
data
|
|
660
|
+
})
|
|
661
|
+
.catch((error) => console.error('[CRDT] update request failed', error))
|
|
696
662
|
}
|
|
697
663
|
})
|
|
698
664
|
|
|
699
|
-
// добавляем документ в хранилище документов
|
|
700
|
-
this.store[opts.storeKey]?.docs.set(entity_id, doc)
|
|
701
|
-
|
|
702
665
|
// добавляем документ в реактивное хранилище
|
|
703
|
-
const json: any =
|
|
666
|
+
const json: any = yjsDoc.getMap().toJSON()
|
|
704
667
|
const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === entity_id)
|
|
705
668
|
if (index !== undefined && index >= 0) {
|
|
706
669
|
if (this.debug) {
|
|
@@ -713,10 +676,8 @@ export class CuboCrdtClient<M> {
|
|
|
713
676
|
}
|
|
714
677
|
|
|
715
678
|
if (opts?.onAfterCreate) {
|
|
716
|
-
await opts.onAfterCreate(
|
|
679
|
+
await opts.onAfterCreate(yjsDoc, update, ctx, opts)
|
|
717
680
|
}
|
|
718
|
-
|
|
719
|
-
this.ensureDocumentAwareness(ctx, opts, doc, entity_id)
|
|
720
681
|
}
|
|
721
682
|
|
|
722
683
|
private async onDocumentUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
@@ -725,7 +686,7 @@ export class CuboCrdtClient<M> {
|
|
|
725
686
|
return
|
|
726
687
|
}
|
|
727
688
|
|
|
728
|
-
const doc = this.
|
|
689
|
+
const doc = this.docs.get(`${ctx.entity}:${ctx.entity_id}`)
|
|
729
690
|
if (!doc) {
|
|
730
691
|
console.warn('[CRDT] onDocumentUpdate: doc not exists', { ctx, opts })
|
|
731
692
|
return
|
|
@@ -734,20 +695,20 @@ export class CuboCrdtClient<M> {
|
|
|
734
695
|
const update = new Uint8Array(ctx.data as any)
|
|
735
696
|
|
|
736
697
|
if (opts?.onBeforeUpdate) {
|
|
737
|
-
const result = await opts.onBeforeUpdate(doc, update, ctx, opts)
|
|
698
|
+
const result = await opts.onBeforeUpdate(doc.doc, update, ctx, opts)
|
|
738
699
|
if (!result) {
|
|
739
700
|
return
|
|
740
701
|
}
|
|
741
702
|
}
|
|
742
703
|
|
|
743
704
|
// обновляем yjs-ный документ
|
|
744
|
-
applyUpdate(doc, update, { react: false })
|
|
705
|
+
applyUpdate(doc.doc, update, { react: false })
|
|
745
706
|
|
|
746
|
-
//
|
|
747
|
-
this.
|
|
707
|
+
// один канонический документ обновляет проекции всех активных подписок
|
|
708
|
+
this.applyDocumentRows(entity_id, doc)
|
|
748
709
|
|
|
749
710
|
if (opts?.onAfterUpdate) {
|
|
750
|
-
await opts.onAfterUpdate(doc, update, ctx, opts)
|
|
711
|
+
await opts.onAfterUpdate(doc.doc, update, ctx, opts)
|
|
751
712
|
}
|
|
752
713
|
}
|
|
753
714
|
|
|
@@ -757,43 +718,34 @@ export class CuboCrdtClient<M> {
|
|
|
757
718
|
return
|
|
758
719
|
}
|
|
759
720
|
|
|
760
|
-
const doc = this.
|
|
721
|
+
const doc = this.docs.get(`${ctx.entity}:${ctx.entity_id}`)
|
|
761
722
|
if (!doc) {
|
|
762
723
|
console.warn('[CRDT] onDocumentDelete: doc not exists', { ctx, opts })
|
|
763
724
|
return
|
|
764
725
|
}
|
|
765
726
|
|
|
766
727
|
if (opts?.onBeforeDelete) {
|
|
767
|
-
const result = await opts.onBeforeDelete(doc, ctx, opts)
|
|
728
|
+
const result = await opts.onBeforeDelete(doc.doc, ctx, opts)
|
|
768
729
|
if (!result) {
|
|
769
730
|
return
|
|
770
731
|
}
|
|
771
732
|
}
|
|
772
733
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (index !== undefined && index >= 0) {
|
|
780
|
-
this.store[opts.storeKey]?.state.rows.splice(index, 1)
|
|
781
|
-
} else {
|
|
782
|
-
if (this.debug) {
|
|
783
|
-
console.log('[CRDT] row not found', index, ctx, opts)
|
|
734
|
+
for (const subscribe_id of doc.subscribes) {
|
|
735
|
+
const subscription = this.subscriptions.get(subscribe_id)
|
|
736
|
+
const item = subscription && this.store[subscription.storeKey]
|
|
737
|
+
const index = item?.state.rows.findIndex((row: any) => row.id === entity_id) ?? -1
|
|
738
|
+
if (item && index >= 0) {
|
|
739
|
+
item.state.rows.splice(index, 1)
|
|
784
740
|
}
|
|
785
741
|
}
|
|
786
742
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
awareness.destroy()
|
|
792
|
-
this.store[opts.storeKey]?.awarenesses?.delete(entity_id)
|
|
793
|
-
}
|
|
743
|
+
doc.awareness?.destroy()
|
|
744
|
+
doc.doc.destroy()
|
|
745
|
+
this.docs.delete(`${ctx.entity}:${ctx.entity_id}`)
|
|
794
746
|
|
|
795
747
|
if (opts?.onAfterDelete) {
|
|
796
|
-
await opts.onAfterDelete(doc, ctx, opts)
|
|
748
|
+
await opts.onAfterDelete(doc.doc, ctx, opts)
|
|
797
749
|
}
|
|
798
750
|
}
|
|
799
751
|
|
|
@@ -828,27 +780,18 @@ export class CuboCrdtClient<M> {
|
|
|
828
780
|
const ids = data.ids.filter((id) => Number.isFinite(id))
|
|
829
781
|
const idsSet = new Set(ids)
|
|
830
782
|
|
|
831
|
-
for (const
|
|
783
|
+
for (const row of item.state.rows) {
|
|
784
|
+
const entity_id = (row as any).id
|
|
785
|
+
|
|
832
786
|
if (idsSet.has(entity_id)) {
|
|
833
787
|
continue
|
|
834
788
|
}
|
|
835
789
|
|
|
836
|
-
|
|
837
|
-
if (awareness) {
|
|
838
|
-
removeAwarenessStates(awareness, [awareness.doc.clientID], 'sync')
|
|
839
|
-
awareness.setLocalState(null)
|
|
840
|
-
awareness.destroy()
|
|
841
|
-
item.awarenesses.delete(entity_id)
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
doc.destroy()
|
|
845
|
-
item.docs.delete(entity_id)
|
|
790
|
+
this.detachDocument(ctx.entity, entity_id, opts.subscribe_id, opts.storeKey)
|
|
846
791
|
}
|
|
847
792
|
|
|
848
793
|
const rowsById = new Map(item.state.rows.map((row: any) => [Number(row.id), row]))
|
|
849
|
-
const orderedRows = ids
|
|
850
|
-
.map((id) => rowsById.get(id))
|
|
851
|
-
.filter((row) => row !== undefined)
|
|
794
|
+
const orderedRows = ids.map((id) => rowsById.get(id)).filter((row) => row !== undefined)
|
|
852
795
|
|
|
853
796
|
item.state.rows.splice(0, item.state.rows.length, ...orderedRows)
|
|
854
797
|
item.state.totals = data.totals || {}
|
|
@@ -865,15 +808,15 @@ export class CuboCrdtClient<M> {
|
|
|
865
808
|
return
|
|
866
809
|
}
|
|
867
810
|
|
|
868
|
-
const
|
|
869
|
-
if (!awareness) {
|
|
811
|
+
const doc = this.docs.get(`${ctx.entity}:${ctx.entity_id}`)
|
|
812
|
+
if (!doc?.awareness) {
|
|
870
813
|
return
|
|
871
814
|
}
|
|
872
815
|
|
|
873
816
|
const update = new Uint8Array(ctx.data as any)
|
|
874
817
|
|
|
875
818
|
try {
|
|
876
|
-
applyAwarenessUpdate(awareness, update, 'remote')
|
|
819
|
+
applyAwarenessUpdate(doc.awareness, update, 'remote')
|
|
877
820
|
} catch (e) {
|
|
878
821
|
console.warn('[CRDT] applyAwarenessUpdate failed ', e)
|
|
879
822
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { Awareness } from 'y-protocols/awareness'
|
|
2
|
+
import { Doc } from 'yjs'
|
|
3
|
+
|
|
1
4
|
import { CuboCrdtClientUseOptions } from '..'
|
|
2
5
|
import { CuboCrdtExposeStrategy } from '../../types'
|
|
3
6
|
|
|
4
7
|
import { CuboCrdtClientBaseOptions } from './utils'
|
|
5
8
|
|
|
9
|
+
export type CuboCrdtClientDoc = {
|
|
10
|
+
doc: Doc
|
|
11
|
+
subscribes: Set<string>
|
|
12
|
+
awareness?: Awareness
|
|
13
|
+
awarenessSubscribes: Set<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export type CuboCrdtClientDocOrigin = {
|
|
7
17
|
// сохранять ли на бэке в дебаунсе
|
|
8
18
|
store?: boolean
|
|
@@ -15,6 +25,9 @@ export type CuboCrdtClientDocOrigin = {
|
|
|
15
25
|
|
|
16
26
|
// куда раскатывать обновления - всем или всем кроме себя
|
|
17
27
|
expose?: CuboCrdtExposeStrategy
|
|
28
|
+
|
|
29
|
+
// подписка, из которой инициировано локальное изменение
|
|
30
|
+
subscribe_id?: string
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
export type CuboCrdtClientDocUpdateOptions = { subscribe_id: string; storeKey: string } & Pick<
|
|
@@ -1,14 +1,8 @@
|
|
|
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
|
|
|
13
7
|
import { CuboCrdtClientBaseOptions } from './utils'
|
|
14
8
|
|
|
@@ -45,7 +39,6 @@ export type CuboCrdtClientSubscribeEvent = {
|
|
|
45
39
|
export type CuboCrdtClientList<T> = {
|
|
46
40
|
subscribe_id: string
|
|
47
41
|
storeKey: string
|
|
48
|
-
docs: ComputedRef<Map<number, Doc> | undefined>
|
|
49
42
|
awarenesses?: ComputedRef<Map<number, Awareness> | undefined>
|
|
50
43
|
subscribe: (filters?: any) => void
|
|
51
44
|
upgrade: (filters?: any) => void
|
|
@@ -64,8 +57,6 @@ export type CuboCrdtClientList<T> = {
|
|
|
64
57
|
export type CuboCrdtClientRow<T> = {
|
|
65
58
|
subscribe_id: string
|
|
66
59
|
storeKey: string
|
|
67
|
-
doc: ComputedRef<Doc | undefined>
|
|
68
|
-
awareness?: ComputedRef<Awareness | undefined>
|
|
69
60
|
subscribe: () => void
|
|
70
61
|
upgrade: (filters?: any) => void
|
|
71
62
|
unsubscribe: (clear?: boolean) => void
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { Reactive } from 'vue'
|
|
2
|
-
import { Doc } from 'yjs'
|
|
3
|
-
import { Awareness } from 'y-protocols/awareness'
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
CuboCrdtKey,
|
|
7
|
-
CuboCrdtListTotals,
|
|
8
|
-
CuboCrdtSubscriptionStatus
|
|
9
|
-
} from '../../types'
|
|
3
|
+
import { CuboCrdtKey, CuboCrdtListTotals, CuboCrdtSubscriptionStatus } from '../../types'
|
|
10
4
|
|
|
11
5
|
export type CuboCrdtClientStoreItem<T> = {
|
|
12
6
|
state: Reactive<{
|
|
@@ -18,8 +12,6 @@ export type CuboCrdtClientStoreItem<T> = {
|
|
|
18
12
|
totals: CuboCrdtListTotals
|
|
19
13
|
revision: number
|
|
20
14
|
}>
|
|
21
|
-
docs: Map<number, Doc>
|
|
22
|
-
awarenesses: Map<number, Awareness>
|
|
23
15
|
}
|
|
24
16
|
export type CuboCrdtClientStore<M> = Partial<{
|
|
25
17
|
[K in string]: CuboCrdtClientStoreItem<CuboCrdtKey<K, M>>
|
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'
|
|
@@ -129,6 +125,29 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
for (const row of data) {
|
|
128
|
+
const subscribe_id = row.origin?.subscribe_id
|
|
129
|
+
const subscribe = subscribe_id && this.subscribes.get(subscribe_id)
|
|
130
|
+
const documentName = `${row.entity}:${row.entity_id}`
|
|
131
|
+
const requiresActiveSubscription = row.action === 'update' || row.action === 'awareness'
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
requiresActiveSubscription &&
|
|
135
|
+
(!subscribe ||
|
|
136
|
+
subscribe.client_id !== client.id ||
|
|
137
|
+
subscribe.entity !== row.entity ||
|
|
138
|
+
!this.documentsBySubscribe.get(subscribe_id)?.has(documentName))
|
|
139
|
+
) {
|
|
140
|
+
if (this.debug) {
|
|
141
|
+
console.warn('[CRDT] ignored update from inactive subscription', {
|
|
142
|
+
action: row.action,
|
|
143
|
+
entity: row.entity,
|
|
144
|
+
entity_id: row.entity_id,
|
|
145
|
+
subscribe_id
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
132
151
|
const update = new Uint8Array(row.data)
|
|
133
152
|
const origin: CuboCrdtServerDocumentOrigin = {
|
|
134
153
|
...(row.origin || {}),
|
|
@@ -197,6 +216,17 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
197
216
|
this.documentEvictions.clear()
|
|
198
217
|
|
|
199
218
|
this.subscribes.forEach((sub) => this.ws.deleteHandler(sub.id))
|
|
219
|
+
|
|
220
|
+
const documents = [...this.documents.values()]
|
|
221
|
+
const results = await Promise.allSettled(documents.map((document) => document.flushStoreQueue()))
|
|
222
|
+
results.forEach((result, index) => {
|
|
223
|
+
if (result.status === 'rejected') {
|
|
224
|
+
console.error('[CRDT] flush document on destroy', documents[index]?.name, result.reason)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
documents.forEach((document) => document.destroy())
|
|
228
|
+
this.documents.clear()
|
|
229
|
+
this.subscribesByDocument.clear()
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
public addClient(client: WsServerSocket<{ auth?: A }>) {
|
|
@@ -501,12 +531,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
501
531
|
const generation = this.subscribeGenerations.get(subscribe_id) || 0
|
|
502
532
|
const forceSync = state.forceSync
|
|
503
533
|
state.forceSync = false
|
|
504
|
-
const reconciled = await this.reconcileSubscribe(
|
|
505
|
-
client,
|
|
506
|
-
subscribe,
|
|
507
|
-
generation,
|
|
508
|
-
forceSync
|
|
509
|
-
)
|
|
534
|
+
const reconciled = await this.reconcileSubscribe(client, subscribe, generation, forceSync)
|
|
510
535
|
if (!reconciled && forceSync) {
|
|
511
536
|
state.forceSync = true
|
|
512
537
|
}
|
|
@@ -518,11 +543,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
518
543
|
return state.running
|
|
519
544
|
}
|
|
520
545
|
|
|
521
|
-
private requestSubscribeRefresh(
|
|
522
|
-
subscribe_id: string,
|
|
523
|
-
immediate = false,
|
|
524
|
-
forceSync = immediate
|
|
525
|
-
) {
|
|
546
|
+
private requestSubscribeRefresh(subscribe_id: string, immediate = false, forceSync = immediate) {
|
|
526
547
|
const state = this.getSubscribeRefreshState(subscribe_id)
|
|
527
548
|
state.dirty = true
|
|
528
549
|
state.forceSync ||= forceSync
|
|
@@ -608,16 +629,8 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
608
629
|
}
|
|
609
630
|
|
|
610
631
|
if (mutation.previousRow) {
|
|
611
|
-
const previousSuitable = this.checkRowIsSutable(
|
|
612
|
-
|
|
613
|
-
subscribe,
|
|
614
|
-
entity
|
|
615
|
-
)
|
|
616
|
-
const currentSuitable = this.checkRowIsSutable(
|
|
617
|
-
mutation.row,
|
|
618
|
-
subscribe,
|
|
619
|
-
entity
|
|
620
|
-
)
|
|
632
|
+
const previousSuitable = this.checkRowIsSutable(mutation.previousRow, subscribe, entity)
|
|
633
|
+
const currentSuitable = this.checkRowIsSutable(mutation.row, subscribe, entity)
|
|
621
634
|
|
|
622
635
|
if (previousSuitable !== currentSuitable) {
|
|
623
636
|
return true
|
|
@@ -901,13 +914,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
901
914
|
|
|
902
915
|
const subscribeSutable = this.checkSubscribeIsSutable(origin, subscribe)
|
|
903
916
|
const documentExistsInSubscribe = this.subscribesByDocument.get(document.name)?.has(subscribe.id)
|
|
904
|
-
const rowSutable = subscribe.paginated
|
|
905
|
-
? documentExistsInSubscribe
|
|
906
|
-
: this.checkRowIsSutable(row, subscribe, entity)
|
|
917
|
+
const rowSutable = subscribe.paginated ? documentExistsInSubscribe : this.checkRowIsSutable(row, subscribe, entity)
|
|
907
918
|
|
|
908
|
-
const sutable = subscribeSutable && (subscribe.paginated
|
|
909
|
-
? documentExistsInSubscribe
|
|
910
|
-
: rowSutable || documentExistsInSubscribe)
|
|
919
|
+
const sutable = subscribeSutable && (subscribe.paginated ? documentExistsInSubscribe : rowSutable || documentExistsInSubscribe)
|
|
911
920
|
|
|
912
921
|
if (sutable) {
|
|
913
922
|
if (this.debug && !rowSutable) {
|
|
@@ -1008,21 +1017,13 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1008
1017
|
}
|
|
1009
1018
|
}
|
|
1010
1019
|
|
|
1011
|
-
private async reconcileSubscribe(
|
|
1012
|
-
client: WsServerSocket,
|
|
1013
|
-
subscribe: CuboCrdtServerSubscribe,
|
|
1014
|
-
generation: number,
|
|
1015
|
-
forceSync: boolean
|
|
1016
|
-
) {
|
|
1020
|
+
private async reconcileSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe, generation: number, forceSync: boolean) {
|
|
1017
1021
|
const fetched = await this.options.fetchRows?.(client, subscribe)
|
|
1018
1022
|
const result = Array.isArray(fetched)
|
|
1019
1023
|
? { rows: fetched, totals: subscribe.totals }
|
|
1020
1024
|
: { rows: fetched?.rows || [], totals: fetched?.totals || subscribe.totals }
|
|
1021
1025
|
|
|
1022
|
-
if (
|
|
1023
|
-
this.subscribes.get(subscribe.id) !== subscribe ||
|
|
1024
|
-
this.subscribeGenerations.get(subscribe.id) !== generation
|
|
1025
|
-
) {
|
|
1026
|
+
if (this.subscribes.get(subscribe.id) !== subscribe || this.subscribeGenerations.get(subscribe.id) !== generation) {
|
|
1026
1027
|
return false
|
|
1027
1028
|
}
|
|
1028
1029
|
|
|
@@ -1095,10 +1096,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1095
1096
|
}
|
|
1096
1097
|
|
|
1097
1098
|
const idsChanged = !areCrdtListIdsEqual(subscribe.row_ids, nextIds)
|
|
1098
|
-
const totalsChanged = !areCrdtListTotalsEqual(
|
|
1099
|
-
subscribe.totals,
|
|
1100
|
-
result.totals
|
|
1101
|
-
)
|
|
1099
|
+
const totalsChanged = !areCrdtListTotalsEqual(subscribe.totals, result.totals)
|
|
1102
1100
|
|
|
1103
1101
|
subscribe.row_ids = nextIds
|
|
1104
1102
|
subscribe.totals = result.totals
|
|
@@ -1137,10 +1135,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
1137
1135
|
if (upgrade.filters !== undefined) {
|
|
1138
1136
|
subscribe.filters = cloneDeep(upgrade.filters || {})
|
|
1139
1137
|
}
|
|
1140
|
-
subscribe.paginated = this.resolvePaginated(
|
|
1141
|
-
subscribe.filters,
|
|
1142
|
-
upgrade.paginated
|
|
1143
|
-
)
|
|
1138
|
+
subscribe.paginated = this.resolvePaginated(subscribe.filters, upgrade.paginated)
|
|
1144
1139
|
|
|
1145
1140
|
return this.requestSubscribeRefresh(subscribe.id, true)
|
|
1146
1141
|
}
|