@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuboapp/crdt",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "CRDT for CUBO",
5
5
  "main": "src/index.ts",
6
6
  "repository": {
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@cuboapp/types": "^2.0.14",
21
21
  "@cuboapp/utils": "1.0.12",
22
- "@cuboapp/ws": "1.0.9",
22
+ "@cuboapp/ws": "1.0.10",
23
23
  "y-protocols": "^1.0.7",
24
24
  "yjs": "^13.6.31"
25
25
  },
@@ -6,7 +6,11 @@ import { applyUpdate, Doc } from 'yjs'
6
6
 
7
7
  import { CUBO_CRDT_EVENT } from '../constants'
8
8
  import { CuboCrdtServerDocumentIncomingAction } from '../server'
9
- import { CuboCrdtKey } from '../types'
9
+ import {
10
+ CuboCrdtKey,
11
+ CuboCrdtListSyncData,
12
+ CuboCrdtSubscriptionStatus
13
+ } from '../types'
10
14
 
11
15
  import { AsyncSerialQueue } from './queue'
12
16
  import {
@@ -28,9 +32,30 @@ export class CuboCrdtClient<M> {
28
32
  private listeners: Map<string, Map<string, (ctx: CuboCrdtClientSubscribeEvent) => void | Promise<void>>> = new Map()
29
33
 
30
34
  // реестр активных подписок — нужен, чтобы переподписаться после реконнекта
31
- private subscriptions = new Map<string, { entity: string; filters?: Record<string, any>; awareness?: boolean }>()
32
- private connectedOnce = false
35
+ private subscriptions = new Map<
36
+ string,
37
+ {
38
+ entity: string
39
+ storeKey: string
40
+ filters?: Record<string, any>
41
+ awareness?: boolean
42
+ paginated?: boolean
43
+ }
44
+ >()
33
45
  private onReconnect = () => this.resubscribeAll()
46
+ private onDisconnect = () => {
47
+ for (const sub of this.subscriptions.values()) {
48
+ const state = this.store[sub.storeKey]?.state
49
+ if (!state) {
50
+ continue
51
+ }
52
+
53
+ state.subscribed = false
54
+ state.loading = true
55
+ state.status = 'loading'
56
+ state.error = undefined
57
+ }
58
+ }
34
59
 
35
60
  private eventQueue = new AsyncSerialQueue({
36
61
  errorMode: 'continue',
@@ -43,12 +68,9 @@ export class CuboCrdtClient<M> {
43
68
  }
44
69
 
45
70
  async start() {
46
- // если сокет уже подключён к моменту старта — считаем текущий коннект «первым»,
47
- // чтобы первое же событие CONNECTED (после обрыва) сработало как переподписка
48
- this.connectedOnce = this.ws.connected
49
-
50
71
  // переподписываемся на все документы при (ре)коннекте сокета
51
72
  this.ws.ee.on(WsClientEvent.CONNECTED, this.onReconnect)
73
+ this.ws.ee.on(WsClientEvent.DISCONNECTED, this.onDisconnect)
52
74
 
53
75
  this.ws.registerHandler(CUBO_CRDT_EVENT.EVENT, ({ message }) => {
54
76
  this.eventQueue
@@ -83,12 +105,12 @@ export class CuboCrdtClient<M> {
83
105
 
84
106
  async stop() {
85
107
  this.ws.ee.off(WsClientEvent.CONNECTED, this.onReconnect)
108
+ this.ws.ee.off(WsClientEvent.DISCONNECTED, this.onDisconnect)
86
109
 
87
110
  this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
88
111
 
89
112
  this.listeners.clear()
90
113
  this.subscriptions.clear()
91
- this.connectedOnce = false
92
114
  }
93
115
 
94
116
  /**
@@ -101,28 +123,59 @@ export class CuboCrdtClient<M> {
101
123
  * навешивать слушатели не нужно.
102
124
  */
103
125
  private resubscribeAll() {
104
- // первый коннект — useList() уже сам отправил SUBSCRIBE, дублировать не нужно
105
- if (!this.connectedOnce) {
106
- this.connectedOnce = true
107
- return
108
- }
109
-
110
126
  if (this.debug) {
111
127
  console.log('[CRDT] reconnected, resubscribing', { count: this.subscriptions.size })
112
128
  }
113
129
 
114
130
  for (const [subscribe_id, sub] of this.subscriptions) {
115
- try {
116
- this.ws.request({
117
- method: CUBO_CRDT_EVENT.SUBSCRIBE,
118
- data: { subscribe_id, entity: sub.entity, filters: sub.filters, awareness: sub.awareness }
119
- })
120
- } catch (e) {
121
- console.error('[CRDT] resubscribe failed', subscribe_id, e)
131
+ const state = this.store[sub.storeKey]?.state
132
+ if (state) {
133
+ state.subscribed = false
134
+ state.loading = true
135
+ state.status = 'loading'
136
+ state.error = undefined
122
137
  }
138
+
139
+ void this.ws
140
+ .request(
141
+ {
142
+ method: CUBO_CRDT_EVENT.SUBSCRIBE,
143
+ data: {
144
+ subscribe_id,
145
+ entity: sub.entity,
146
+ filters: sub.filters,
147
+ awareness: sub.awareness,
148
+ paginated: sub.paginated
149
+ }
150
+ },
151
+ { wait: true, timeout: 10_000 }
152
+ )
153
+ .catch((e) => {
154
+ const currentState = this.store[sub.storeKey]?.state
155
+ if (currentState) {
156
+ currentState.subscribed = false
157
+ currentState.loading = false
158
+ currentState.status = 'error'
159
+ currentState.error = e
160
+ }
161
+
162
+ if (this.debug) {
163
+ console.error('[CRDT] resubscribe failed', subscribe_id, e)
164
+ }
165
+ })
123
166
  }
124
167
  }
125
168
 
169
+ private resolvePaginated(filters?: Record<string, any>, explicit?: boolean) {
170
+ if (explicit !== undefined) {
171
+ return explicit
172
+ }
173
+
174
+ const page = Number(filters?.page)
175
+ const limit = Number(filters?.limit)
176
+ return Number.isFinite(page) && page > 0 && Number.isFinite(limit) && limit > 0
177
+ }
178
+
126
179
  public useComputedList<K extends Extract<keyof M, string>>(entity: K) {
127
180
  return computed(() => (this.store[entity]?.state.rows || []) as M[K][])
128
181
  }
@@ -146,7 +199,12 @@ export class CuboCrdtClient<M> {
146
199
  if (this.store[storeKey] === undefined) {
147
200
  const state = reactive({
148
201
  rows: [],
149
- subscribed: false
202
+ subscribed: false,
203
+ loading: false,
204
+ status: 'idle' as CuboCrdtSubscriptionStatus,
205
+ error: undefined,
206
+ totals: {},
207
+ revision: 0
150
208
  })
151
209
 
152
210
  this.store[storeKey] = {
@@ -158,14 +216,18 @@ export class CuboCrdtClient<M> {
158
216
 
159
217
  // подписка
160
218
  const subscribe = (filters?: Record<string, any>) => {
161
- if (this.store[storeKey]?.state.subscribed) {
219
+ const currentState = this.store[storeKey]?.state
220
+ if (currentState?.subscribed || currentState?.loading) {
162
221
  return
163
222
  }
164
223
 
165
- // ставим сразу флаг, потому что не может быть ошибок (считаем так)
166
- this.store[storeKey]!.state.subscribed = true
224
+ this.store[storeKey]!.state.subscribed = false
225
+ this.store[storeKey]!.state.loading = true
226
+ this.store[storeKey]!.state.status = 'loading'
227
+ this.store[storeKey]!.state.error = undefined
167
228
 
168
229
  filters = filters !== undefined ? filters : opts?.filters
230
+ const paginated = this.resolvePaginated(filters, opts?.paginated)
169
231
 
170
232
  if (this.debug) {
171
233
  console.log('[CRDT] subscribe', { entity, opts, filters })
@@ -189,15 +251,43 @@ export class CuboCrdtClient<M> {
189
251
  })
190
252
 
191
253
  // регистрируем подписку, чтобы переподписаться после реконнекта
192
- this.subscriptions.set(subscribe_id, { entity: `${entity}`, filters, awareness: opts?.awareness })
254
+ this.subscriptions.set(subscribe_id, {
255
+ entity: `${entity}`,
256
+ storeKey,
257
+ filters,
258
+ awareness: opts?.awareness,
259
+ paginated
260
+ })
193
261
 
194
262
  // подписываемся на бэке (если сокет ещё не подключён — переподпишемся по событию CONNECTED)
195
- try {
196
- this.ws.request({ method: CUBO_CRDT_EVENT.SUBSCRIBE, data: { subscribe_id, entity, filters, awareness: opts?.awareness } })
197
- } catch (e) {
198
- if (this.debug) {
199
- console.warn('[CRDT] subscribe deferred until reconnect', subscribe_id, e)
200
- }
263
+ if (this.ws.connected) {
264
+ void this.ws
265
+ .request(
266
+ {
267
+ method: CUBO_CRDT_EVENT.SUBSCRIBE,
268
+ data: {
269
+ subscribe_id,
270
+ entity,
271
+ filters,
272
+ awareness: opts?.awareness,
273
+ paginated
274
+ }
275
+ },
276
+ { wait: true, timeout: 10_000 }
277
+ )
278
+ .catch((e) => {
279
+ const state = this.store[storeKey]?.state
280
+ if (state) {
281
+ state.subscribed = false
282
+ state.loading = false
283
+ state.status = 'error'
284
+ state.error = e
285
+ }
286
+
287
+ if (this.debug) {
288
+ console.warn('[CRDT] subscribe deferred until reconnect', subscribe_id, e)
289
+ }
290
+ })
201
291
  }
202
292
  }
203
293
 
@@ -211,18 +301,23 @@ export class CuboCrdtClient<M> {
211
301
  this.subscriptions.delete(subscribe_id)
212
302
 
213
303
  // отписываемся на бэке
214
- try {
215
- this.ws.request({ method: CUBO_CRDT_EVENT.UNSUBSCRIBE, data: { subscribe_id } })
216
- } catch (e) {
217
- if (this.debug) {
218
- console.warn('[CRDT] unsubscribe request failed (socket down)', subscribe_id, e)
219
- }
304
+ if (this.ws.connected) {
305
+ void this.ws
306
+ .request({ method: CUBO_CRDT_EVENT.UNSUBSCRIBE, data: { subscribe_id } })
307
+ .catch((e) => {
308
+ if (this.debug) {
309
+ console.warn('[CRDT] unsubscribe request failed (socket down)', subscribe_id, e)
310
+ }
311
+ })
220
312
  }
221
313
 
222
314
  // отписываемся на фронте
223
315
  this.listeners.get(entity)?.delete(subscribe_id)
224
316
 
225
317
  this.store[storeKey]!.state.subscribed = false
318
+ this.store[storeKey]!.state.loading = false
319
+ this.store[storeKey]!.state.status = 'idle'
320
+ this.store[storeKey]!.state.error = undefined
226
321
 
227
322
  // очищаем стор
228
323
  if (clear !== false) {
@@ -263,6 +358,12 @@ export class CuboCrdtClient<M> {
263
358
  docs: computed(() => this.store[storeKey]?.docs),
264
359
  awarenesses: opts?.awareness ? computed(() => this.store[storeKey]?.awarenesses) : undefined,
265
360
  subscribed: () => computed(() => this.store[storeKey]?.state.subscribed || false),
361
+ loading: () => computed(() => this.store[storeKey]?.state.loading || false),
362
+ ready: () => computed(() => this.store[storeKey]?.state.status === 'ready'),
363
+ status: () => computed(() => this.store[storeKey]?.state.status || 'idle'),
364
+ error: () => computed(() => this.store[storeKey]?.state.error),
365
+ totals: () => computed(() => this.store[storeKey]?.state.totals || {}),
366
+ revision: () => computed(() => this.store[storeKey]?.state.revision || 0),
266
367
  rows: () => computed(() => this.store[storeKey]?.state.rows as T[]),
267
368
  rowsById: () => computed(() => keyBy(this.store[storeKey]?.state.rows as T[], 'id' as keyof T))
268
369
  }
@@ -275,7 +376,20 @@ export class CuboCrdtClient<M> {
275
376
  ): CuboCrdtClientRow<T> {
276
377
  // console.log('store key', opts?.storeKey ?? `${entity}:${id}`)
277
378
 
278
- const { subscribe_id, storeKey, docs, awarenesses, subscribe, upgrade, unsubscribe, subscribed } = this.useList(entity, {
379
+ const {
380
+ subscribe_id,
381
+ storeKey,
382
+ docs,
383
+ awarenesses,
384
+ subscribe,
385
+ upgrade,
386
+ unsubscribe,
387
+ subscribed,
388
+ loading,
389
+ ready,
390
+ status,
391
+ error
392
+ } = this.useList(entity, {
279
393
  ...opts,
280
394
  filters: opts?.filters ?? { id },
281
395
  storeKey: opts?.storeKey ?? `${entity}:${id}`
@@ -290,6 +404,10 @@ export class CuboCrdtClient<M> {
290
404
  upgrade,
291
405
  unsubscribe,
292
406
  subscribed,
407
+ loading,
408
+ ready,
409
+ status,
410
+ error,
293
411
  row: () => {
294
412
  return computed(() => {
295
413
  return (this.store[storeKey]?.state.rows?.[0] ?? null) as T
@@ -354,9 +472,39 @@ export class CuboCrdtClient<M> {
354
472
  const sub = this.subscriptions.get(subscribe_id)
355
473
  if (sub) {
356
474
  sub.filters = filters
475
+ sub.paginated = this.resolvePaginated(filters, sub.paginated)
476
+ const state = this.store[sub.storeKey]?.state
477
+ if (state) {
478
+ state.loading = true
479
+ state.status = 'loading'
480
+ state.error = undefined
481
+ }
357
482
  }
358
483
 
359
- this.ws.request({ method: CUBO_CRDT_EVENT.UPGRADE, data: { subscribe_id, filters } })
484
+ if (!this.ws.connected) {
485
+ return
486
+ }
487
+
488
+ void this.ws
489
+ .request(
490
+ {
491
+ method: CUBO_CRDT_EVENT.UPGRADE,
492
+ data: { subscribe_id, filters, paginated: sub?.paginated }
493
+ },
494
+ { wait: true, timeout: 10_000 }
495
+ )
496
+ .catch((e) => {
497
+ const state = sub && this.store[sub.storeKey]?.state
498
+ if (state) {
499
+ state.loading = false
500
+ state.status = 'error'
501
+ state.error = e
502
+ }
503
+
504
+ if (this.debug) {
505
+ console.warn('[CRDT] upgrade deferred until reconnect', subscribe_id, e)
506
+ }
507
+ })
360
508
  }
361
509
 
362
510
  // апсертит строку в реактивное хранилище из текущего состояния yjs-документа
@@ -376,7 +524,12 @@ export class CuboCrdtClient<M> {
376
524
  }
377
525
 
378
526
  private async onDocumentCreate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
379
- const existing = this.store[opts.storeKey]?.docs.get(ctx.entity_id)
527
+ const entity_id = ctx.entity_id
528
+ if (entity_id === undefined) {
529
+ return
530
+ }
531
+
532
+ const existing = this.store[opts.storeKey]?.docs.get(entity_id)
380
533
  if (existing) {
381
534
  // документ уже есть локально (типичный кейс — повторный SUBSCRIBE после реконнекта).
382
535
  // вместо игнорирования вмёрживаем входящее состояние в существующий yjs-документ
@@ -386,7 +539,7 @@ export class CuboCrdtClient<M> {
386
539
  }
387
540
 
388
541
  applyUpdate(existing, new Uint8Array(ctx.data as any), { react: false })
389
- this.applyRowJson(opts.storeKey, ctx.entity_id, existing)
542
+ this.applyRowJson(opts.storeKey, entity_id, existing)
390
543
  return
391
544
  }
392
545
 
@@ -410,7 +563,7 @@ export class CuboCrdtClient<M> {
410
563
  const data: CuboCrdtServerDocumentIncomingAction = {
411
564
  action: 'update',
412
565
  entity: ctx.entity,
413
- entity_id: ctx.entity_id,
566
+ entity_id,
414
567
  data: Array.from(update),
415
568
  origin: {
416
569
  // по умолчанию 'other' — исходную подписку исключаем (её yjs-документ уже
@@ -430,14 +583,14 @@ export class CuboCrdtClient<M> {
430
583
  })
431
584
 
432
585
  // добавляем документ в хранилище документов
433
- this.store[opts.storeKey]?.docs.set(ctx.entity_id, doc)
586
+ this.store[opts.storeKey]?.docs.set(entity_id, doc)
434
587
 
435
588
  // добавляем документ в реактивное хранилище
436
589
  const json: any = doc.getMap().toJSON()
437
- const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === ctx.entity_id)
590
+ const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === entity_id)
438
591
  if (index !== undefined && index >= 0) {
439
592
  if (this.debug) {
440
- console.warn('[CRDT] onDocumentCreate - row already exists: "' + ctx.entity_id + '"', { ctx, opts })
593
+ console.warn('[CRDT] onDocumentCreate - row already exists: "' + entity_id + '"', { ctx, opts })
441
594
  }
442
595
 
443
596
  this.store[opts.storeKey]!.state.rows[index] = json
@@ -450,7 +603,7 @@ export class CuboCrdtClient<M> {
450
603
  }
451
604
 
452
605
  if (opts?.awareness) {
453
- let awareness = this.store[opts.storeKey]?.awarenesses.get(ctx.entity_id)
606
+ let awareness = this.store[opts.storeKey]?.awarenesses.get(entity_id)
454
607
 
455
608
  if (!awareness) {
456
609
  awareness = new Awareness(doc)
@@ -470,7 +623,7 @@ export class CuboCrdtClient<M> {
470
623
  data: {
471
624
  action: 'awareness',
472
625
  entity: ctx.entity,
473
- entity_id: ctx.entity_id,
626
+ entity_id,
474
627
  data: Array.from(update),
475
628
  // 'other' — не шлём свой же курсор обратно исходной подписке
476
629
  origin: { expose: 'other', subscribe_id: opts.subscribe_id }
@@ -478,13 +631,18 @@ export class CuboCrdtClient<M> {
478
631
  })
479
632
  })
480
633
 
481
- this.store[opts.storeKey]?.awarenesses.set(ctx.entity_id, awareness)
634
+ this.store[opts.storeKey]?.awarenesses.set(entity_id, awareness)
482
635
  }
483
636
  }
484
637
  }
485
638
 
486
639
  private async onDocumentUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
487
- const doc = this.store[opts.storeKey]?.docs.get(ctx.entity_id)
640
+ const entity_id = ctx.entity_id
641
+ if (entity_id === undefined) {
642
+ return
643
+ }
644
+
645
+ const doc = this.store[opts.storeKey]?.docs.get(entity_id)
488
646
  if (!doc) {
489
647
  console.warn('[CRDT] onDocumentUpdate: doc not exists', { ctx, opts })
490
648
  return
@@ -503,7 +661,7 @@ export class CuboCrdtClient<M> {
503
661
  applyUpdate(doc, update, { react: false })
504
662
 
505
663
  // обновляем документ в реактивном хранилище
506
- this.applyRowJson(opts.storeKey, ctx.entity_id, doc)
664
+ this.applyRowJson(opts.storeKey, entity_id, doc)
507
665
 
508
666
  if (opts?.onAfterUpdate) {
509
667
  await opts.onAfterUpdate(doc, update, ctx, opts)
@@ -511,7 +669,12 @@ export class CuboCrdtClient<M> {
511
669
  }
512
670
 
513
671
  private async onDocumentDelete(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
514
- const doc = this.store[opts.storeKey]?.docs.get(ctx.entity_id)
672
+ const entity_id = ctx.entity_id
673
+ if (entity_id === undefined) {
674
+ return
675
+ }
676
+
677
+ const doc = this.store[opts.storeKey]?.docs.get(entity_id)
515
678
  if (!doc) {
516
679
  console.warn('[CRDT] onDocumentDelete: doc not exists', { ctx, opts })
517
680
  return
@@ -527,9 +690,9 @@ export class CuboCrdtClient<M> {
527
690
  // console.log('onDocumentDelete', ctx, opts)
528
691
 
529
692
  doc.destroy()
530
- this.store[opts.storeKey]?.docs.delete(ctx.entity_id)
693
+ this.store[opts.storeKey]?.docs.delete(entity_id)
531
694
 
532
- const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === ctx.entity_id)
695
+ const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === entity_id)
533
696
  if (index !== undefined && index >= 0) {
534
697
  this.store[opts.storeKey]?.state.rows.splice(index, 1)
535
698
  } else {
@@ -538,12 +701,12 @@ export class CuboCrdtClient<M> {
538
701
  }
539
702
  }
540
703
 
541
- const awareness = this.store[opts.storeKey]?.awarenesses.get(ctx.entity_id)
704
+ const awareness = this.store[opts.storeKey]?.awarenesses.get(entity_id)
542
705
  if (awareness) {
543
706
  removeAwarenessStates(awareness, [awareness.doc.clientID], 'unsubscribe')
544
707
 
545
708
  awareness.destroy()
546
- this.store[opts.storeKey]?.awarenesses?.delete(ctx.entity_id)
709
+ this.store[opts.storeKey]?.awarenesses?.delete(entity_id)
547
710
  }
548
711
 
549
712
  if (opts?.onAfterDelete) {
@@ -566,11 +729,60 @@ export class CuboCrdtClient<M> {
566
729
  case 'awareness':
567
730
  await this.onAwarenessUpdate(ctx, opts)
568
731
  break
732
+ case 'sync':
733
+ this.onListSync(ctx, opts)
734
+ break
569
735
  }
570
736
  }
571
737
 
738
+ private onListSync(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
739
+ const data = ctx.data as CuboCrdtListSyncData | undefined
740
+ const item = this.store[opts.storeKey]
741
+ if (!data || Array.isArray(data) || !Array.isArray(data.ids) || !item) {
742
+ return
743
+ }
744
+
745
+ const ids = data.ids.filter((id) => Number.isFinite(id))
746
+ const idsSet = new Set(ids)
747
+
748
+ for (const [entity_id, doc] of item.docs) {
749
+ if (idsSet.has(entity_id)) {
750
+ continue
751
+ }
752
+
753
+ const awareness = item.awarenesses.get(entity_id)
754
+ if (awareness) {
755
+ removeAwarenessStates(awareness, [awareness.doc.clientID], 'sync')
756
+ awareness.setLocalState(null)
757
+ awareness.destroy()
758
+ item.awarenesses.delete(entity_id)
759
+ }
760
+
761
+ doc.destroy()
762
+ item.docs.delete(entity_id)
763
+ }
764
+
765
+ const rowsById = new Map(item.state.rows.map((row: any) => [Number(row.id), row]))
766
+ const orderedRows = ids
767
+ .map((id) => rowsById.get(id))
768
+ .filter((row) => row !== undefined)
769
+
770
+ item.state.rows.splice(0, item.state.rows.length, ...orderedRows)
771
+ item.state.totals = data.totals || {}
772
+ item.state.revision = data.revision
773
+ item.state.loading = false
774
+ item.state.status = 'ready'
775
+ item.state.error = undefined
776
+ item.state.subscribed = true
777
+ }
778
+
572
779
  private onAwarenessUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
573
- const awareness = this.store[opts.storeKey]?.awarenesses?.get(ctx.entity_id)
780
+ const entity_id = ctx.entity_id
781
+ if (entity_id === undefined) {
782
+ return
783
+ }
784
+
785
+ const awareness = this.store[opts.storeKey]?.awarenesses?.get(entity_id)
574
786
  if (!awareness) {
575
787
  return
576
788
  }
@@ -3,7 +3,12 @@ import { ComputedRef } from 'vue'
3
3
  import { Awareness } from 'y-protocols/awareness'
4
4
  import { type Doc } from 'yjs'
5
5
 
6
- import { CuboCrdtAction } from '../../types'
6
+ import {
7
+ CuboCrdtAction,
8
+ CuboCrdtListSyncData,
9
+ CuboCrdtListTotals,
10
+ CuboCrdtSubscriptionStatus
11
+ } from '../../types'
7
12
 
8
13
  import { CuboCrdtClientBaseOptions } from './utils'
9
14
 
@@ -21,14 +26,15 @@ export type CuboCrdtClientUseOptions = CuboCrdtClientBaseOptions & {
21
26
  storeKey?: string
22
27
  filters?: Record<string, any>
23
28
  awareness?: boolean
29
+ paginated?: boolean
24
30
  }
25
31
 
26
32
  export type CuboCrdtClientSubscribeEvent = {
27
33
  action: CuboCrdtAction
28
34
  entity: string
29
- entity_id: number
35
+ entity_id?: number
30
36
  subscribe_id?: string
31
- data?: number[]
37
+ data?: number[] | CuboCrdtListSyncData
32
38
  }
33
39
 
34
40
  export type CuboCrdtClientList<T> = {
@@ -40,6 +46,12 @@ export type CuboCrdtClientList<T> = {
40
46
  upgrade: (filters?: any) => void
41
47
  unsubscribe: (clear?: boolean) => void
42
48
  subscribed: () => ComputedRef<boolean>
49
+ loading: () => ComputedRef<boolean>
50
+ ready: () => ComputedRef<boolean>
51
+ status: () => ComputedRef<CuboCrdtSubscriptionStatus>
52
+ error: () => ComputedRef<unknown>
53
+ totals: () => ComputedRef<CuboCrdtListTotals>
54
+ revision: () => ComputedRef<number>
43
55
  rows: () => ComputedRef<T[]>
44
56
  rowsById: () => ComputedRef<Record<string, T>>
45
57
  }
@@ -53,5 +65,9 @@ export type CuboCrdtClientRow<T> = {
53
65
  upgrade: (filters?: any) => void
54
66
  unsubscribe: (clear?: boolean) => void
55
67
  subscribed: () => ComputedRef<boolean>
68
+ loading: () => ComputedRef<boolean>
69
+ ready: () => ComputedRef<boolean>
70
+ status: () => ComputedRef<CuboCrdtSubscriptionStatus>
71
+ error: () => ComputedRef<unknown>
56
72
  row: () => ComputedRef<T>
57
73
  }
@@ -2,12 +2,21 @@ import { Reactive } from 'vue'
2
2
  import { Doc } from 'yjs'
3
3
  import { Awareness } from 'y-protocols/awareness'
4
4
 
5
- import { CuboCrdtKey } from '../../types'
5
+ import {
6
+ CuboCrdtKey,
7
+ CuboCrdtListTotals,
8
+ CuboCrdtSubscriptionStatus
9
+ } from '../../types'
6
10
 
7
11
  export type CuboCrdtClientStoreItem<T> = {
8
12
  state: Reactive<{
9
13
  rows: T[]
10
14
  subscribed: boolean
15
+ loading: boolean
16
+ status: CuboCrdtSubscriptionStatus
17
+ error?: unknown
18
+ totals: CuboCrdtListTotals
19
+ revision: number
11
20
  }>
12
21
  docs: Map<number, Doc>
13
22
  awarenesses: Map<number, Awareness>
@@ -1,4 +1,4 @@
1
- import { debounce, pick } from '@cuboapp/utils'
1
+ import { cloneDeep, debounce, pick } from '@cuboapp/utils'
2
2
  import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'
3
3
  import { Awareness, encodeAwarenessUpdate } from 'y-protocols/awareness'
4
4
 
@@ -6,6 +6,7 @@ import { CuboCrdtServerDocumentOptions, CuboCrdtServerDocumentOrigin } from '../
6
6
 
7
7
  export class CuboCrdtServerDocument {
8
8
  private ydoc: Doc
9
+ private persistedState: object = {}
9
10
  public awareness: Awareness
10
11
  public awarenessBySubscribe = new Map<string, Set<number>>()
11
12
 
@@ -42,6 +43,8 @@ export class CuboCrdtServerDocument {
42
43
  })
43
44
  }
44
45
 
46
+ this.persistedState = cloneDeep(opts.initialState ?? {})
47
+
45
48
  // записываем исходное состояние
46
49
  this.write(opts.initialState ?? {})
47
50
 
@@ -91,6 +94,14 @@ export class CuboCrdtServerDocument {
91
94
  return this.getMap().toJSON()
92
95
  }
93
96
 
97
+ getPersistedState() {
98
+ return cloneDeep(this.persistedState)
99
+ }
100
+
101
+ setPersistedState(row: object) {
102
+ this.persistedState = cloneDeep(row)
103
+ }
104
+
94
105
  async write(dto: object, origin?: CuboCrdtServerDocumentOrigin) {
95
106
  const map = this.getMap()
96
107
 
@@ -120,8 +131,12 @@ export class CuboCrdtServerDocument {
120
131
  this.ydoc.destroy()
121
132
  }
122
133
 
123
- public store(body: object, origin: CuboCrdtServerDocumentOrigin) {
124
- this.opts?.onStore?.(body, origin)
134
+ public async store(body: object, origin: CuboCrdtServerDocumentOrigin) {
135
+ const previousRow = this.getPersistedState()
136
+ const row = this.getJson()
137
+
138
+ await this.opts?.onStore?.(body, origin, { previousRow, row })
139
+ this.setPersistedState(row)
125
140
  }
126
141
  public debounceStore = debounce(this.store.bind(this), 300)
127
142
  }