@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.
@@ -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).
@@ -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, { document, client, origin })
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
- this.pushDocumentAction('upsert', entity, document, document.stateAsUpdate)
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 = this.checkRowIsSutable(row, subscribe, entity)
815
+ const rowSutable = subscribe.paginated
816
+ ? documentExistsInSubscribe
817
+ : this.checkRowIsSutable(row, subscribe, entity)
572
818
 
573
- const sutable = subscribeSutable && (rowSutable || documentExistsInSubscribe)
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 pushSubscribeDocuments(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
675
- // получаем строки под фильтры ЭТОЙ подписки
676
- const rows = await this.options.fetchRows?.(client, subscribe)
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
- // 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) {
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, (row as any).id)
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.subscribesUpgrading.has(subscribe.id)) {
700
- return this.subscribesUpgrading.get(subscribe.id)
1017
+ if (this.debug) {
1018
+ console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
701
1019
  }
702
1020
 
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
- })
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.subscribesUpgrading.get(subscribe.id)
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.subscribesIniting.has(subscribe.id)) {
736
- return this.subscribesIniting.get(subscribe.id)
1034
+ if (this.debug) {
1035
+ console.log('[CRDT] init subscribe ' + client.id, subscribe)
737
1036
  }
738
1037
 
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)
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?: (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
+ }