@cuboapp/crdt 1.0.15 → 1.0.17

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.
@@ -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 { checkRowIsSutable } from '../utils'
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 subscribesIniting = new Map<string, Promise<void>>()
35
- private subscribesUpgrading = new Map<string, Promise<void>>()
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).
@@ -364,6 +597,21 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
364
597
 
365
598
  let document = this.documents.get(documentName)
366
599
  if (document) {
600
+ // Освежаем существующий документ строкой из БД.
601
+ //
602
+ // Раньше документ возвращался как есть, а подписка отправляла клиенту его
603
+ // stateAsUpdate — то есть КЭШ документа, а не только что прочитанную строку.
604
+ // Документ, проспавший мутацию, оставался устаревшим навсегда: даже перезагрузка
605
+ // страницы отдавала старое значение, потому что reconcile брал его же.
606
+ //
607
+ // Стало заметно после того, как документы без подписчиков начали удаляться:
608
+ // документ может быть создан, остаться без подписок, пропустить мутации и
609
+ // «воскреснуть» на новой подписке уже неактуальным.
610
+ //
611
+ // store: false — это не правка от клиента, а синхронизация с БД, писать обратно нечего.
612
+ document.write(row, { expose: 'all', store: false })
613
+ document.setPersistedState(row)
614
+
367
615
  return document
368
616
  }
369
617
 
@@ -377,12 +625,18 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
377
625
  onAwarenessUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
378
626
  this.pushDocumentAction('awareness', entity, document!, data, origin)
379
627
  },
380
- onStore: (item: object, origin: CuboCrdtServerDocumentOrigin) => {
628
+ onStore: (item: object, origin: CuboCrdtServerDocumentOrigin, store) => {
381
629
  const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
382
630
  const client = subscribe && this.clients.get(subscribe.client_id)
383
631
 
384
632
  if (client) {
385
- return this.options?.storeRow?.(entity as any, entity_id, item as any, { document, client, origin })
633
+ return this.options?.storeRow?.(entity as any, entity_id, item as any, {
634
+ document,
635
+ client,
636
+ origin,
637
+ previousRow: store.previousRow as any,
638
+ row: store.row as any
639
+ })
386
640
  }
387
641
  }
388
642
  })
@@ -411,8 +665,14 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
411
665
 
412
666
  if (!existed) {
413
667
  this.pushDocumentAction('create', entity, document, document.stateAsUpdate)
414
- } else {
415
- this.pushDocumentAction('upsert', entity, document, document.stateAsUpdate)
668
+ }
669
+
670
+ // Для уже существующего документа освежать состояние здесь не нужно —
671
+ // это делает getOrCreateDocument, одинаково для всех путей (рассылка и подписка).
672
+ // write() сам пропускает неизменившиеся ключи, так что лишних update-эвентов нет.
673
+
674
+ if (!this.subscribesByDocument.get(documentName)?.size) {
675
+ this.checkDocumentNeedRemove(documentName)
416
676
  }
417
677
 
418
678
  return document
@@ -568,9 +828,13 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
568
828
 
569
829
  const subscribeSutable = this.checkSubscribeIsSutable(origin, subscribe)
570
830
  const documentExistsInSubscribe = this.subscribesByDocument.get(document.name)?.has(subscribe.id)
571
- const rowSutable = this.checkRowIsSutable(row, subscribe, entity)
831
+ const rowSutable = subscribe.paginated
832
+ ? documentExistsInSubscribe
833
+ : this.checkRowIsSutable(row, subscribe, entity)
572
834
 
573
- const sutable = subscribeSutable && (rowSutable || documentExistsInSubscribe)
835
+ const sutable = subscribeSutable && (subscribe.paginated
836
+ ? documentExistsInSubscribe
837
+ : rowSutable || documentExistsInSubscribe)
574
838
 
575
839
  if (sutable) {
576
840
  if (this.debug && !rowSutable) {
@@ -671,95 +935,123 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
671
935
  }
672
936
  }
673
937
 
674
- private async pushSubscribeDocuments(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
675
- // получаем строки под фильтры ЭТОЙ подписки
676
- const rows = await this.options.fetchRows?.(client, subscribe)
938
+ private async reconcileSubscribe(
939
+ client: WsServerSocket,
940
+ subscribe: CuboCrdtServerSubscribe,
941
+ generation: number,
942
+ forceSync: boolean
943
+ ) {
944
+ const fetched = await this.options.fetchRows?.(client, subscribe)
945
+ const result = Array.isArray(fetched)
946
+ ? { rows: fetched, totals: subscribe.totals }
947
+ : { rows: fetched?.rows || [], totals: fetched?.totals || subscribe.totals }
948
+
949
+ if (
950
+ this.subscribes.get(subscribe.id) !== subscribe ||
951
+ this.subscribeGenerations.get(subscribe.id) !== generation
952
+ ) {
953
+ return false
954
+ }
955
+
956
+ const rowsById = new Map<number, any>()
957
+ const nextIds: number[] = []
958
+
959
+ for (const row of result.rows) {
960
+ const id = Number((row as any)?.id)
961
+ if (!Number.isFinite(id) || rowsById.has(id)) {
962
+ continue
963
+ }
964
+
965
+ rowsById.set(id, row)
966
+ nextIds.push(id)
967
+ }
968
+
969
+ const previousIds = new Set(subscribe.row_ids)
970
+ const nextIdsSet = new Set(nextIds)
971
+
972
+ for (const entity_id of subscribe.row_ids) {
973
+ if (nextIdsSet.has(entity_id)) {
974
+ continue
975
+ }
976
+
977
+ const documentName = `${subscribe.entity}:${entity_id}`
978
+ this.unlinkSubscribeFromDocument(subscribe.id, documentName)
979
+ this.checkDocumentNeedRemove(documentName)
980
+ }
981
+
982
+ for (const entity_id of nextIds) {
983
+ const row = rowsById.get(entity_id)
984
+ if (!row) {
985
+ continue
986
+ }
677
987
 
678
- // Доставляем документы ТОЧЕЧНО инициирующей подписке. Раньше это шло через
679
- // ensureDocument -> pushDocumentAction('upsert') с полным сканом всех подписок сущности
680
- // НА КАЖДУЮ строку из fetchRows (до 10k) — главный усилитель нагрузки при (ре)подписке.
681
- // Строки уже отфильтрованы под фильтры подписки в fetchRows, поэтому повторная
682
- // проверка/рассылка остальным не нужна: живые create/update придут через onAfterCreate.
683
- for (const row of rows || []) {
684
- if ((row as any)?.id == null) {
988
+ if (previousIds.has(entity_id)) {
989
+ this.linkSubscribeToDocument(subscribe.id, `${subscribe.entity}:${entity_id}`)
685
990
  continue
686
991
  }
687
992
 
688
993
  const document = this.getOrCreateDocument(subscribe.entity as E, row, subscribe.awareness)
689
- this.sendDocumentToSubscribe(subscribe, document, (row as any).id)
994
+ this.sendDocumentToSubscribe(subscribe, document, entity_id)
690
995
  }
996
+
997
+ const idsChanged = !areCrdtListIdsEqual(subscribe.row_ids, nextIds)
998
+ const totalsChanged = !areCrdtListTotalsEqual(
999
+ subscribe.totals,
1000
+ result.totals
1001
+ )
1002
+
1003
+ subscribe.row_ids = nextIds
1004
+ subscribe.totals = result.totals
1005
+
1006
+ if (!forceSync && !idsChanged && !totalsChanged) {
1007
+ return true
1008
+ }
1009
+
1010
+ subscribe.revision += 1
1011
+ const data: CuboCrdtListSyncData = {
1012
+ ids: nextIds,
1013
+ totals: subscribe.totals,
1014
+ revision: subscribe.revision
1015
+ }
1016
+
1017
+ this.sendToClient(subscribe.client_id, {
1018
+ action: 'sync',
1019
+ entity: subscribe.entity,
1020
+ subscribe_id: subscribe.id,
1021
+ data
1022
+ })
1023
+
1024
+ return true
691
1025
  }
692
1026
 
693
1027
  // апгрейд подписки (фильтры поменялись например)
694
1028
  private async upgradeSubscribe(
695
1029
  client: WsServerSocket,
696
1030
  subscribe: CuboCrdtServerSubscribe,
697
- upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters'>>
1031
+ upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters' | 'paginated'>>
698
1032
  ) {
699
- if (this.subscribesUpgrading.has(subscribe.id)) {
700
- return this.subscribesUpgrading.get(subscribe.id)
1033
+ if (this.debug) {
1034
+ console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
701
1035
  }
702
1036
 
703
- this.subscribesUpgrading.set(
704
- subscribe.id,
705
- new Promise<void>(async (resolve, reject) => {
706
- try {
707
- if (this.debug) {
708
- console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
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
- })
1037
+ if (upgrade.filters !== undefined) {
1038
+ subscribe.filters = cloneDeep(upgrade.filters || {})
1039
+ }
1040
+ subscribe.paginated = this.resolvePaginated(
1041
+ subscribe.filters,
1042
+ upgrade.paginated
728
1043
  )
729
1044
 
730
- return this.subscribesUpgrading.get(subscribe.id)
1045
+ return this.requestSubscribeRefresh(subscribe.id, true)
731
1046
  }
732
1047
 
733
1048
  // инциализация подписки
734
1049
  private async initSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
735
- if (this.subscribesIniting.has(subscribe.id)) {
736
- return this.subscribesIniting.get(subscribe.id)
1050
+ if (this.debug) {
1051
+ console.log('[CRDT] init subscribe ' + client.id, subscribe)
737
1052
  }
738
1053
 
739
- this.subscribesIniting.set(
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)
1054
+ return this.requestSubscribeRefresh(subscribe.id, true)
763
1055
  }
764
1056
 
765
1057
  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?: (body: object, origin: CuboCrdtServerDocumentOrigin) => void
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
- fetchRows?: (client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) => Promise<M[E][]>
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: boolean
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
- } & Pick<CuboCrdtServerSubscribeDto, 'entity' | 'filters' | 'awareness'>
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
+ }