@cuboapp/crdt 1.0.15 → 1.0.16
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 +2 -2
- package/src/client/index.ts +269 -57
- package/src/client/types/index.ts +19 -3
- package/src/client/types/store.ts +10 -1
- package/src/server/document/index.ts +18 -3
- package/src/server/index.ts +357 -81
- package/src/server/types/document.ts +10 -1
- package/src/server/types/index.ts +20 -1
- package/src/server/types/subscribe.ts +15 -2
- package/src/types/index.ts +24 -1
- package/src/utils/index.ts +24 -0
package/src/server/index.ts
CHANGED
|
@@ -3,8 +3,12 @@ import { WsServerSocket } from '@cuboapp/ws'
|
|
|
3
3
|
import { applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
4
4
|
|
|
5
5
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
6
|
-
import { CuboCrdtAction } from '../types'
|
|
7
|
-
import {
|
|
6
|
+
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtMutation } from '../types'
|
|
7
|
+
import {
|
|
8
|
+
areCrdtListIdsEqual,
|
|
9
|
+
areCrdtListTotalsEqual,
|
|
10
|
+
checkRowIsSutable
|
|
11
|
+
} from '../utils'
|
|
8
12
|
|
|
9
13
|
import { CuboCrdtClientDocOrigin } from '../client'
|
|
10
14
|
import { CuboCrdtServerDocument } from './document'
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
CuboCrdtServerOptions,
|
|
15
19
|
CuboCrdtServerSubscribe,
|
|
16
20
|
CuboCrdtServerSubscribeDto,
|
|
21
|
+
CuboCrdtSubscribeRefreshState,
|
|
17
22
|
CuboCrdtServerUnsubscribeDto,
|
|
18
23
|
CuboCrdtSocketClient
|
|
19
24
|
} from './types'
|
|
@@ -31,8 +36,8 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
31
36
|
private subscribes = new Map<string, CuboCrdtServerSubscribe>()
|
|
32
37
|
private subscribesByClient = new Map<string, Set<string>>()
|
|
33
38
|
private subscribesByEntity = new Map<E, Set<string>>()
|
|
34
|
-
private
|
|
35
|
-
private
|
|
39
|
+
private subscribeRefreshStates = new Map<string, CuboCrdtSubscribeRefreshState>()
|
|
40
|
+
private subscribeGenerations = new Map<string, number>()
|
|
36
41
|
|
|
37
42
|
private documents = new Map<string, CuboCrdtServerDocument>()
|
|
38
43
|
private subscribesByDocument = new Map<string, Set<string>>()
|
|
@@ -44,18 +49,27 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
44
49
|
this.entities.forEach((e) => this.subscribesByEntity.set(e, new Set()))
|
|
45
50
|
|
|
46
51
|
this.ws.registerHandler(CUBO_CRDT_EVENT.SUBSCRIBE, async ({ client, message }) => {
|
|
47
|
-
const { subscribe_id: id, entity, filters, awareness = false } = message.data as CuboCrdtServerSubscribeDto
|
|
52
|
+
const { subscribe_id: id, entity, filters, awareness = false, paginated } = message.data as CuboCrdtServerSubscribeDto
|
|
48
53
|
|
|
49
54
|
if (this.debug) {
|
|
50
55
|
console.log('[CRDT] subscribe', entity, filters)
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
const existing = this.subscribes.get(id)
|
|
59
|
+
if (existing) {
|
|
60
|
+
this.cleanSubscribe(id)
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
const subscribe = {
|
|
54
64
|
id,
|
|
55
65
|
client_id: client.id,
|
|
56
66
|
entity,
|
|
57
|
-
filters,
|
|
58
|
-
awareness
|
|
67
|
+
filters: cloneDeep(filters || {}),
|
|
68
|
+
awareness,
|
|
69
|
+
paginated: this.resolvePaginated(filters, paginated),
|
|
70
|
+
row_ids: [],
|
|
71
|
+
totals: {},
|
|
72
|
+
revision: 0
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
this.subscribes.set(id, subscribe)
|
|
@@ -67,11 +81,13 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
67
81
|
this.subscribesByEntity.get(entity as E)?.add(id)
|
|
68
82
|
|
|
69
83
|
// инициализируем подписку
|
|
70
|
-
this.initSubscribe(client, subscribe)
|
|
84
|
+
await this.initSubscribe(client, subscribe)
|
|
85
|
+
|
|
86
|
+
return { subscribe_id: id, revision: subscribe.revision }
|
|
71
87
|
})
|
|
72
88
|
|
|
73
89
|
this.ws.registerHandler(CUBO_CRDT_EVENT.UPGRADE, async ({ client, message }) => {
|
|
74
|
-
const { subscribe_id: id, entity, filters } = message.data as CuboCrdtServerSubscribeDto
|
|
90
|
+
const { subscribe_id: id, entity, filters, paginated } = message.data as CuboCrdtServerSubscribeDto
|
|
75
91
|
|
|
76
92
|
if (this.debug) {
|
|
77
93
|
console.log('[CRDT] upgrade', entity, filters)
|
|
@@ -83,7 +99,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
// обновляем подписку
|
|
86
|
-
this.upgradeSubscribe(client, subscribe, { filters })
|
|
102
|
+
await this.upgradeSubscribe(client, subscribe, { filters, paginated })
|
|
103
|
+
|
|
104
|
+
return { subscribe_id: id, revision: subscribe.revision }
|
|
87
105
|
})
|
|
88
106
|
|
|
89
107
|
this.ws.registerHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE, async ({ message }) => {
|
|
@@ -148,9 +166,14 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
148
166
|
|
|
149
167
|
public async destroy() {
|
|
150
168
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.SUBSCRIBE)
|
|
169
|
+
this.ws.deleteHandler(CUBO_CRDT_EVENT.UPGRADE)
|
|
151
170
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE)
|
|
152
171
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
|
|
153
172
|
|
|
173
|
+
this.subscribeRefreshStates.forEach((state) => clearTimeout(state.timer))
|
|
174
|
+
this.subscribeRefreshStates.clear()
|
|
175
|
+
this.subscribeGenerations.clear()
|
|
176
|
+
|
|
154
177
|
this.subscribes.forEach((sub) => this.ws.deleteHandler(sub.id))
|
|
155
178
|
}
|
|
156
179
|
|
|
@@ -192,6 +215,21 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
192
215
|
return this.options.batch?.debounce ?? 100
|
|
193
216
|
}
|
|
194
217
|
|
|
218
|
+
private get paginationDebounce() {
|
|
219
|
+
return this.options.pagination?.debounce ?? 75
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private resolvePaginated(filters?: Record<string, any>, explicit?: boolean) {
|
|
223
|
+
if (explicit !== undefined) {
|
|
224
|
+
return explicit
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const page = Number(filters?.page)
|
|
228
|
+
const limit = Number(filters?.limit)
|
|
229
|
+
|
|
230
|
+
return Number.isFinite(page) && page > 0 && Number.isFinite(limit) && limit > 0
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
private sendQueueToClient(client: CuboCrdtSocketClient<A>) {
|
|
196
234
|
// снимаем запланированный флаш — мы отправляем прямо сейчас
|
|
197
235
|
clearTimeout(this.sendTimeouts.get(client.id))
|
|
@@ -329,6 +367,11 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
329
367
|
// удаляем обратный индекс
|
|
330
368
|
this.documentsBySubscribe.delete(subscribe_id)
|
|
331
369
|
|
|
370
|
+
const refreshState = this.subscribeRefreshStates.get(subscribe_id)
|
|
371
|
+
clearTimeout(refreshState?.timer)
|
|
372
|
+
this.subscribeRefreshStates.delete(subscribe_id)
|
|
373
|
+
this.subscribeGenerations.delete(subscribe_id)
|
|
374
|
+
|
|
332
375
|
// удаляем подписки
|
|
333
376
|
this.subscribes.delete(subscribe_id)
|
|
334
377
|
this.subscribesByClient.get(subscribe.client_id)?.delete(subscribe_id)
|
|
@@ -355,6 +398,196 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
355
398
|
})
|
|
356
399
|
}
|
|
357
400
|
|
|
401
|
+
private getSubscribeRefreshState(subscribe_id: string) {
|
|
402
|
+
let state = this.subscribeRefreshStates.get(subscribe_id)
|
|
403
|
+
if (!state) {
|
|
404
|
+
state = { dirty: false, forceSync: false }
|
|
405
|
+
this.subscribeRefreshStates.set(subscribe_id, state)
|
|
406
|
+
}
|
|
407
|
+
return state
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private bumpSubscribeGeneration(subscribe_id: string) {
|
|
411
|
+
const generation = (this.subscribeGenerations.get(subscribe_id) || 0) + 1
|
|
412
|
+
this.subscribeGenerations.set(subscribe_id, generation)
|
|
413
|
+
return generation
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private runSubscribeRefresh(subscribe_id: string) {
|
|
417
|
+
const state = this.getSubscribeRefreshState(subscribe_id)
|
|
418
|
+
if (state.running) {
|
|
419
|
+
return state.running
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
state.running = (async () => {
|
|
423
|
+
while (state.dirty) {
|
|
424
|
+
state.dirty = false
|
|
425
|
+
|
|
426
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
427
|
+
if (!subscribe) {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const client = this.clients.get(subscribe.client_id)
|
|
432
|
+
if (!client) {
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const generation = this.subscribeGenerations.get(subscribe_id) || 0
|
|
437
|
+
const forceSync = state.forceSync
|
|
438
|
+
state.forceSync = false
|
|
439
|
+
const reconciled = await this.reconcileSubscribe(
|
|
440
|
+
client,
|
|
441
|
+
subscribe,
|
|
442
|
+
generation,
|
|
443
|
+
forceSync
|
|
444
|
+
)
|
|
445
|
+
if (!reconciled && forceSync) {
|
|
446
|
+
state.forceSync = true
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
})().finally(() => {
|
|
450
|
+
state.running = undefined
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return state.running
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private requestSubscribeRefresh(
|
|
457
|
+
subscribe_id: string,
|
|
458
|
+
immediate = false,
|
|
459
|
+
forceSync = immediate
|
|
460
|
+
) {
|
|
461
|
+
const state = this.getSubscribeRefreshState(subscribe_id)
|
|
462
|
+
state.dirty = true
|
|
463
|
+
state.forceSync ||= forceSync
|
|
464
|
+
this.bumpSubscribeGeneration(subscribe_id)
|
|
465
|
+
|
|
466
|
+
if (immediate) {
|
|
467
|
+
clearTimeout(state.timer)
|
|
468
|
+
state.timer = undefined
|
|
469
|
+
return this.runSubscribeRefresh(subscribe_id)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!state.timer && !state.running) {
|
|
473
|
+
state.timer = setTimeout(() => {
|
|
474
|
+
state.timer = undefined
|
|
475
|
+
this.runSubscribeRefresh(subscribe_id).catch((e) => {
|
|
476
|
+
if (this.debug) {
|
|
477
|
+
console.error('[CRDT] refresh subscribe', subscribe_id, e)
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
}, this.paginationDebounce)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return state.running ?? Promise.resolve()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private getSubscribeQueryKeys(filters?: Record<string, any>) {
|
|
487
|
+
const keys = new Set<string>()
|
|
488
|
+
|
|
489
|
+
Object.keys(filters || {}).forEach((key) => {
|
|
490
|
+
if (!['page', 'limit', 'sort', 'with'].includes(key)) {
|
|
491
|
+
keys.add(key)
|
|
492
|
+
|
|
493
|
+
const rangeKey = key.replace(/_(start|end)$/, '')
|
|
494
|
+
if (rangeKey !== key) {
|
|
495
|
+
keys.add(rangeKey)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (key.endsWith('_ids')) {
|
|
499
|
+
keys.add(key.slice(0, -1))
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
const appendSort = (sort: unknown) => {
|
|
505
|
+
if (Array.isArray(sort)) {
|
|
506
|
+
sort.forEach(appendSort)
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
if (typeof sort !== 'string') {
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
sort
|
|
514
|
+
.split(',')
|
|
515
|
+
.map((item) => item.trim().replace(/^-/, '').split(':')[0])
|
|
516
|
+
.filter(Boolean)
|
|
517
|
+
.forEach((key) => keys.add(key))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
appendSort(filters?.sort)
|
|
521
|
+
|
|
522
|
+
return keys
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private shouldRefreshSubscribe(entity: E, subscribe: CuboCrdtServerSubscribe, mutation: CuboCrdtMutation<M[E]>) {
|
|
526
|
+
if (!subscribe.paginated) {
|
|
527
|
+
return false
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (this.options.shouldRefreshSubscribe) {
|
|
531
|
+
return this.options.shouldRefreshSubscribe({ entity, subscribe, mutation })
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (mutation.action !== 'update' || !mutation.changedKeys?.length) {
|
|
535
|
+
return true
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (mutation.previousRow) {
|
|
539
|
+
const previousSuitable = this.checkRowIsSutable(
|
|
540
|
+
mutation.previousRow,
|
|
541
|
+
subscribe,
|
|
542
|
+
entity
|
|
543
|
+
)
|
|
544
|
+
const currentSuitable = this.checkRowIsSutable(
|
|
545
|
+
mutation.row,
|
|
546
|
+
subscribe,
|
|
547
|
+
entity
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if (previousSuitable !== currentSuitable) {
|
|
551
|
+
return true
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (subscribe.filters?.search) {
|
|
556
|
+
return true
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const queryKeys = this.getSubscribeQueryKeys(subscribe.filters)
|
|
560
|
+
return mutation.changedKeys.some((key) => queryKeys.has(key))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Уведомляет пагинированные подписки о сохранённой мутации.
|
|
565
|
+
* Пересчитываются только реально открытые страницы; несколько мутаций
|
|
566
|
+
* схлопываются debounce-ом в один запрос на подписку.
|
|
567
|
+
*/
|
|
568
|
+
public notifyMutation(entity: E, mutation: CuboCrdtMutation<M[E]>) {
|
|
569
|
+
for (const subscribe_id of this.subscribesByEntity.get(entity) || []) {
|
|
570
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
571
|
+
if (subscribe && this.shouldRefreshSubscribe(entity, subscribe, mutation)) {
|
|
572
|
+
this.requestSubscribeRefresh(subscribe_id)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Принудительно инвалидирует все открытые пагинированные окна сущности.
|
|
579
|
+
* Используется для вычисляемых полей и зависимых сущностей, когда changedKeys
|
|
580
|
+
* исходной строки недостаточно для определения влияния на запрос.
|
|
581
|
+
*/
|
|
582
|
+
public invalidate(entity: E) {
|
|
583
|
+
for (const subscribe_id of this.subscribesByEntity.get(entity) || []) {
|
|
584
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
585
|
+
if (subscribe?.paginated) {
|
|
586
|
+
this.requestSubscribeRefresh(subscribe_id)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
358
591
|
// создаёт (или возвращает существующий) серверный документ БЕЗ рассылки подписчикам.
|
|
359
592
|
// Используется как для рассылочного пути (ensureDocument), так и для точечной
|
|
360
593
|
// доставки на init подписки (pushSubscribeDocuments).
|
|
@@ -377,12 +610,18 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
377
610
|
onAwarenessUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
378
611
|
this.pushDocumentAction('awareness', entity, document!, data, origin)
|
|
379
612
|
},
|
|
380
|
-
onStore: (item: object, origin: CuboCrdtServerDocumentOrigin) => {
|
|
613
|
+
onStore: (item: object, origin: CuboCrdtServerDocumentOrigin, store) => {
|
|
381
614
|
const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
|
|
382
615
|
const client = subscribe && this.clients.get(subscribe.client_id)
|
|
383
616
|
|
|
384
617
|
if (client) {
|
|
385
|
-
return this.options?.storeRow?.(entity as any, entity_id, item as any, {
|
|
618
|
+
return this.options?.storeRow?.(entity as any, entity_id, item as any, {
|
|
619
|
+
document,
|
|
620
|
+
client,
|
|
621
|
+
origin,
|
|
622
|
+
previousRow: store.previousRow as any,
|
|
623
|
+
row: store.row as any
|
|
624
|
+
})
|
|
386
625
|
}
|
|
387
626
|
}
|
|
388
627
|
})
|
|
@@ -412,7 +651,12 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
412
651
|
if (!existed) {
|
|
413
652
|
this.pushDocumentAction('create', entity, document, document.stateAsUpdate)
|
|
414
653
|
} else {
|
|
415
|
-
|
|
654
|
+
document.write(row, { expose: 'all', store: false })
|
|
655
|
+
document.setPersistedState(row)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!this.subscribesByDocument.get(documentName)?.size) {
|
|
659
|
+
this.checkDocumentNeedRemove(documentName)
|
|
416
660
|
}
|
|
417
661
|
|
|
418
662
|
return document
|
|
@@ -568,9 +812,13 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
568
812
|
|
|
569
813
|
const subscribeSutable = this.checkSubscribeIsSutable(origin, subscribe)
|
|
570
814
|
const documentExistsInSubscribe = this.subscribesByDocument.get(document.name)?.has(subscribe.id)
|
|
571
|
-
const rowSutable =
|
|
815
|
+
const rowSutable = subscribe.paginated
|
|
816
|
+
? documentExistsInSubscribe
|
|
817
|
+
: this.checkRowIsSutable(row, subscribe, entity)
|
|
572
818
|
|
|
573
|
-
const sutable = subscribeSutable && (
|
|
819
|
+
const sutable = subscribeSutable && (subscribe.paginated
|
|
820
|
+
? documentExistsInSubscribe
|
|
821
|
+
: rowSutable || documentExistsInSubscribe)
|
|
574
822
|
|
|
575
823
|
if (sutable) {
|
|
576
824
|
if (this.debug && !rowSutable) {
|
|
@@ -671,95 +919,123 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
671
919
|
}
|
|
672
920
|
}
|
|
673
921
|
|
|
674
|
-
private async
|
|
675
|
-
|
|
676
|
-
|
|
922
|
+
private async reconcileSubscribe(
|
|
923
|
+
client: WsServerSocket,
|
|
924
|
+
subscribe: CuboCrdtServerSubscribe,
|
|
925
|
+
generation: number,
|
|
926
|
+
forceSync: boolean
|
|
927
|
+
) {
|
|
928
|
+
const fetched = await this.options.fetchRows?.(client, subscribe)
|
|
929
|
+
const result = Array.isArray(fetched)
|
|
930
|
+
? { rows: fetched, totals: subscribe.totals }
|
|
931
|
+
: { rows: fetched?.rows || [], totals: fetched?.totals || subscribe.totals }
|
|
932
|
+
|
|
933
|
+
if (
|
|
934
|
+
this.subscribes.get(subscribe.id) !== subscribe ||
|
|
935
|
+
this.subscribeGenerations.get(subscribe.id) !== generation
|
|
936
|
+
) {
|
|
937
|
+
return false
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const rowsById = new Map<number, any>()
|
|
941
|
+
const nextIds: number[] = []
|
|
942
|
+
|
|
943
|
+
for (const row of result.rows) {
|
|
944
|
+
const id = Number((row as any)?.id)
|
|
945
|
+
if (!Number.isFinite(id) || rowsById.has(id)) {
|
|
946
|
+
continue
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
rowsById.set(id, row)
|
|
950
|
+
nextIds.push(id)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const previousIds = new Set(subscribe.row_ids)
|
|
954
|
+
const nextIdsSet = new Set(nextIds)
|
|
955
|
+
|
|
956
|
+
for (const entity_id of subscribe.row_ids) {
|
|
957
|
+
if (nextIdsSet.has(entity_id)) {
|
|
958
|
+
continue
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const documentName = `${subscribe.entity}:${entity_id}`
|
|
962
|
+
this.unlinkSubscribeFromDocument(subscribe.id, documentName)
|
|
963
|
+
this.checkDocumentNeedRemove(documentName)
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
for (const entity_id of nextIds) {
|
|
967
|
+
const row = rowsById.get(entity_id)
|
|
968
|
+
if (!row) {
|
|
969
|
+
continue
|
|
970
|
+
}
|
|
677
971
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
// НА КАЖДУЮ строку из fetchRows (до 10k) — главный усилитель нагрузки при (ре)подписке.
|
|
681
|
-
// Строки уже отфильтрованы под фильтры подписки в fetchRows, поэтому повторная
|
|
682
|
-
// проверка/рассылка остальным не нужна: живые create/update придут через onAfterCreate.
|
|
683
|
-
for (const row of rows || []) {
|
|
684
|
-
if ((row as any)?.id == null) {
|
|
972
|
+
if (previousIds.has(entity_id)) {
|
|
973
|
+
this.linkSubscribeToDocument(subscribe.id, `${subscribe.entity}:${entity_id}`)
|
|
685
974
|
continue
|
|
686
975
|
}
|
|
687
976
|
|
|
688
977
|
const document = this.getOrCreateDocument(subscribe.entity as E, row, subscribe.awareness)
|
|
689
|
-
this.sendDocumentToSubscribe(subscribe, document,
|
|
978
|
+
this.sendDocumentToSubscribe(subscribe, document, entity_id)
|
|
690
979
|
}
|
|
980
|
+
|
|
981
|
+
const idsChanged = !areCrdtListIdsEqual(subscribe.row_ids, nextIds)
|
|
982
|
+
const totalsChanged = !areCrdtListTotalsEqual(
|
|
983
|
+
subscribe.totals,
|
|
984
|
+
result.totals
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
subscribe.row_ids = nextIds
|
|
988
|
+
subscribe.totals = result.totals
|
|
989
|
+
|
|
990
|
+
if (!forceSync && !idsChanged && !totalsChanged) {
|
|
991
|
+
return true
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
subscribe.revision += 1
|
|
995
|
+
const data: CuboCrdtListSyncData = {
|
|
996
|
+
ids: nextIds,
|
|
997
|
+
totals: subscribe.totals,
|
|
998
|
+
revision: subscribe.revision
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
this.sendToClient(subscribe.client_id, {
|
|
1002
|
+
action: 'sync',
|
|
1003
|
+
entity: subscribe.entity,
|
|
1004
|
+
subscribe_id: subscribe.id,
|
|
1005
|
+
data
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
return true
|
|
691
1009
|
}
|
|
692
1010
|
|
|
693
1011
|
// апгрейд подписки (фильтры поменялись например)
|
|
694
1012
|
private async upgradeSubscribe(
|
|
695
1013
|
client: WsServerSocket,
|
|
696
1014
|
subscribe: CuboCrdtServerSubscribe,
|
|
697
|
-
upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters'>>
|
|
1015
|
+
upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters' | 'paginated'>>
|
|
698
1016
|
) {
|
|
699
|
-
if (this.
|
|
700
|
-
|
|
1017
|
+
if (this.debug) {
|
|
1018
|
+
console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
|
|
701
1019
|
}
|
|
702
1020
|
|
|
703
|
-
|
|
704
|
-
subscribe.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (upgrade.filters) {
|
|
712
|
-
subscribe.filters = cloneDeep(upgrade.filters || {})
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
await this.pushSubscribeDocuments(client, subscribe)
|
|
716
|
-
|
|
717
|
-
resolve()
|
|
718
|
-
} catch (e) {
|
|
719
|
-
if (this.debug) {
|
|
720
|
-
console.error('[CRDT] upgrade subscribe', e)
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
reject(e)
|
|
724
|
-
} finally {
|
|
725
|
-
this.subscribesUpgrading.delete(subscribe.id)
|
|
726
|
-
}
|
|
727
|
-
})
|
|
1021
|
+
if (upgrade.filters !== undefined) {
|
|
1022
|
+
subscribe.filters = cloneDeep(upgrade.filters || {})
|
|
1023
|
+
}
|
|
1024
|
+
subscribe.paginated = this.resolvePaginated(
|
|
1025
|
+
subscribe.filters,
|
|
1026
|
+
upgrade.paginated
|
|
728
1027
|
)
|
|
729
1028
|
|
|
730
|
-
return this.
|
|
1029
|
+
return this.requestSubscribeRefresh(subscribe.id, true)
|
|
731
1030
|
}
|
|
732
1031
|
|
|
733
1032
|
// инциализация подписки
|
|
734
1033
|
private async initSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
|
|
735
|
-
if (this.
|
|
736
|
-
|
|
1034
|
+
if (this.debug) {
|
|
1035
|
+
console.log('[CRDT] init subscribe ' + client.id, subscribe)
|
|
737
1036
|
}
|
|
738
1037
|
|
|
739
|
-
this.
|
|
740
|
-
subscribe.id,
|
|
741
|
-
new Promise<void>(async (resolve, reject) => {
|
|
742
|
-
try {
|
|
743
|
-
if (this.debug) {
|
|
744
|
-
console.log('[CRDT] init subscribe ' + client.id, subscribe)
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
await this.pushSubscribeDocuments(client, subscribe)
|
|
748
|
-
|
|
749
|
-
resolve()
|
|
750
|
-
} catch (e) {
|
|
751
|
-
if (this.debug) {
|
|
752
|
-
console.error('[CRDT] init subscribe', e)
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
reject(e)
|
|
756
|
-
} finally {
|
|
757
|
-
this.subscribesIniting.delete(subscribe.id)
|
|
758
|
-
}
|
|
759
|
-
})
|
|
760
|
-
)
|
|
761
|
-
|
|
762
|
-
return this.subscribesIniting.get(subscribe.id)
|
|
1038
|
+
return this.requestSubscribeRefresh(subscribe.id, true)
|
|
763
1039
|
}
|
|
764
1040
|
|
|
765
1041
|
private get debug() {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { CuboCrdtAction, CuboCrdtExposeStrategy } from '../../types'
|
|
2
2
|
|
|
3
|
+
export type CuboCrdtServerDocumentStoreContext = {
|
|
4
|
+
previousRow: object
|
|
5
|
+
row: object
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export type CuboCrdtServerDocumentOptions = {
|
|
4
9
|
name: string
|
|
5
|
-
onStore?: (
|
|
10
|
+
onStore?: (
|
|
11
|
+
body: object,
|
|
12
|
+
origin: CuboCrdtServerDocumentOrigin,
|
|
13
|
+
context: CuboCrdtServerDocumentStoreContext
|
|
14
|
+
) => void | Promise<void>
|
|
6
15
|
// onCreate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
|
|
7
16
|
onUpdate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
|
|
8
17
|
onAwarenessUpdate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WsServer, WsServerSocket } from '@cuboapp/ws'
|
|
2
|
+
import { CuboCrdtListTotals, CuboCrdtMutation } from '../../types'
|
|
2
3
|
import { CuboCrdtServerDocument } from '../document'
|
|
3
4
|
import { CuboCrdtServerDocumentOrigin } from './document'
|
|
4
5
|
import { CuboCrdtServerSubscribe } from './subscribe'
|
|
@@ -6,6 +7,11 @@ import { CuboCrdtServerSubscribe } from './subscribe'
|
|
|
6
7
|
export * from './document'
|
|
7
8
|
export * from './subscribe'
|
|
8
9
|
|
|
10
|
+
export type CuboCrdtServerFetchResult<T> = {
|
|
11
|
+
rows: T[]
|
|
12
|
+
totals?: CuboCrdtListTotals
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export type CuboCrdtServerOptions<M, A, E extends Extract<keyof M, string>> = {
|
|
10
16
|
ws: WsServer
|
|
11
17
|
entities: E[]
|
|
@@ -19,8 +25,19 @@ export type CuboCrdtServerOptions<M, A, E extends Extract<keyof M, string>> = {
|
|
|
19
25
|
maxSize?: number
|
|
20
26
|
debounce?: number
|
|
21
27
|
}
|
|
22
|
-
|
|
28
|
+
pagination?: {
|
|
29
|
+
debounce?: number
|
|
30
|
+
}
|
|
31
|
+
fetchRows?: (
|
|
32
|
+
client: WsServerSocket,
|
|
33
|
+
subscribe: CuboCrdtServerSubscribe
|
|
34
|
+
) => Promise<M[E][] | CuboCrdtServerFetchResult<M[E]>>
|
|
23
35
|
checkRowIsSutable?: (baseState: boolean, ctx: { entity: E; subscribe: CuboCrdtServerSubscribe; row: any }) => boolean
|
|
36
|
+
shouldRefreshSubscribe?: (ctx: {
|
|
37
|
+
entity: E
|
|
38
|
+
subscribe: CuboCrdtServerSubscribe
|
|
39
|
+
mutation: CuboCrdtMutation<M[E]>
|
|
40
|
+
}) => boolean
|
|
24
41
|
storeRow?: <K extends E>(
|
|
25
42
|
entity: K,
|
|
26
43
|
entity_id: number,
|
|
@@ -29,6 +46,8 @@ export type CuboCrdtServerOptions<M, A, E extends Extract<keyof M, string>> = {
|
|
|
29
46
|
document: CuboCrdtServerDocument
|
|
30
47
|
client: CuboCrdtSocketClient<A>
|
|
31
48
|
origin: CuboCrdtServerDocumentOrigin
|
|
49
|
+
previousRow: M[K]
|
|
50
|
+
row: M[K]
|
|
32
51
|
}
|
|
33
52
|
) => Promise<void> | void
|
|
34
53
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CuboCrdtListTotals } from '../../types'
|
|
2
|
+
|
|
1
3
|
export type CuboCrdtServerUnsubscribeDto = {
|
|
2
4
|
subscribe_id: string
|
|
3
5
|
}
|
|
@@ -7,10 +9,21 @@ export type CuboCrdtServerSubscribeDto = {
|
|
|
7
9
|
filters: {
|
|
8
10
|
[K in 'id' | string]: any
|
|
9
11
|
}
|
|
10
|
-
awareness
|
|
12
|
+
awareness?: boolean
|
|
13
|
+
paginated?: boolean
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export type CuboCrdtServerSubscribe = {
|
|
14
17
|
id: string
|
|
15
18
|
client_id: string
|
|
16
|
-
|
|
19
|
+
row_ids: number[]
|
|
20
|
+
totals: CuboCrdtListTotals
|
|
21
|
+
revision: number
|
|
22
|
+
} & Pick<CuboCrdtServerSubscribeDto, 'entity' | 'filters' | 'awareness' | 'paginated'>
|
|
23
|
+
|
|
24
|
+
export type CuboCrdtSubscribeRefreshState = {
|
|
25
|
+
dirty: boolean
|
|
26
|
+
forceSync: boolean
|
|
27
|
+
timer?: ReturnType<typeof setTimeout>
|
|
28
|
+
running?: Promise<void>
|
|
29
|
+
}
|