@cuboapp/crdt 1.0.14 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuboapp/crdt",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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,9 +563,13 @@ 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: {
569
+ // по умолчанию 'other' — исходную подписку исключаем (её yjs-документ уже
570
+ // применил изменение локально). Без этого апдейты от TipTap/y-prosemirror
571
+ // приходят без expose и сервер эхом шлёт их обратно самому автору.
572
+ expose: 'other',
416
573
  ...pick(origin || {}, ['store', 'keys', 'expose']),
417
574
  subscribe_id: opts?.subscribe_id
418
575
  }
@@ -426,14 +583,14 @@ export class CuboCrdtClient<M> {
426
583
  })
427
584
 
428
585
  // добавляем документ в хранилище документов
429
- this.store[opts.storeKey]?.docs.set(ctx.entity_id, doc)
586
+ this.store[opts.storeKey]?.docs.set(entity_id, doc)
430
587
 
431
588
  // добавляем документ в реактивное хранилище
432
589
  const json: any = doc.getMap().toJSON()
433
- 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)
434
591
  if (index !== undefined && index >= 0) {
435
592
  if (this.debug) {
436
- console.warn('[CRDT] onDocumentCreate - row already exists: "' + ctx.entity_id + '"', { ctx, opts })
593
+ console.warn('[CRDT] onDocumentCreate - row already exists: "' + entity_id + '"', { ctx, opts })
437
594
  }
438
595
 
439
596
  this.store[opts.storeKey]!.state.rows[index] = json
@@ -446,7 +603,7 @@ export class CuboCrdtClient<M> {
446
603
  }
447
604
 
448
605
  if (opts?.awareness) {
449
- let awareness = this.store[opts.storeKey]?.awarenesses.get(ctx.entity_id)
606
+ let awareness = this.store[opts.storeKey]?.awarenesses.get(entity_id)
450
607
 
451
608
  if (!awareness) {
452
609
  awareness = new Awareness(doc)
@@ -466,20 +623,26 @@ export class CuboCrdtClient<M> {
466
623
  data: {
467
624
  action: 'awareness',
468
625
  entity: ctx.entity,
469
- entity_id: ctx.entity_id,
626
+ entity_id,
470
627
  data: Array.from(update),
471
- origin: { subscribe_id: opts.subscribe_id }
628
+ // 'other' не шлём свой же курсор обратно исходной подписке
629
+ origin: { expose: 'other', subscribe_id: opts.subscribe_id }
472
630
  }
473
631
  })
474
632
  })
475
633
 
476
- this.store[opts.storeKey]?.awarenesses.set(ctx.entity_id, awareness)
634
+ this.store[opts.storeKey]?.awarenesses.set(entity_id, awareness)
477
635
  }
478
636
  }
479
637
  }
480
638
 
481
639
  private async onDocumentUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
482
- 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)
483
646
  if (!doc) {
484
647
  console.warn('[CRDT] onDocumentUpdate: doc not exists', { ctx, opts })
485
648
  return
@@ -498,7 +661,7 @@ export class CuboCrdtClient<M> {
498
661
  applyUpdate(doc, update, { react: false })
499
662
 
500
663
  // обновляем документ в реактивном хранилище
501
- this.applyRowJson(opts.storeKey, ctx.entity_id, doc)
664
+ this.applyRowJson(opts.storeKey, entity_id, doc)
502
665
 
503
666
  if (opts?.onAfterUpdate) {
504
667
  await opts.onAfterUpdate(doc, update, ctx, opts)
@@ -506,7 +669,12 @@ export class CuboCrdtClient<M> {
506
669
  }
507
670
 
508
671
  private async onDocumentDelete(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
509
- 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)
510
678
  if (!doc) {
511
679
  console.warn('[CRDT] onDocumentDelete: doc not exists', { ctx, opts })
512
680
  return
@@ -522,9 +690,9 @@ export class CuboCrdtClient<M> {
522
690
  // console.log('onDocumentDelete', ctx, opts)
523
691
 
524
692
  doc.destroy()
525
- this.store[opts.storeKey]?.docs.delete(ctx.entity_id)
693
+ this.store[opts.storeKey]?.docs.delete(entity_id)
526
694
 
527
- 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)
528
696
  if (index !== undefined && index >= 0) {
529
697
  this.store[opts.storeKey]?.state.rows.splice(index, 1)
530
698
  } else {
@@ -533,12 +701,12 @@ export class CuboCrdtClient<M> {
533
701
  }
534
702
  }
535
703
 
536
- const awareness = this.store[opts.storeKey]?.awarenesses.get(ctx.entity_id)
704
+ const awareness = this.store[opts.storeKey]?.awarenesses.get(entity_id)
537
705
  if (awareness) {
538
706
  removeAwarenessStates(awareness, [awareness.doc.clientID], 'unsubscribe')
539
707
 
540
708
  awareness.destroy()
541
- this.store[opts.storeKey]?.awarenesses?.delete(ctx.entity_id)
709
+ this.store[opts.storeKey]?.awarenesses?.delete(entity_id)
542
710
  }
543
711
 
544
712
  if (opts?.onAfterDelete) {
@@ -561,11 +729,60 @@ export class CuboCrdtClient<M> {
561
729
  case 'awareness':
562
730
  await this.onAwarenessUpdate(ctx, opts)
563
731
  break
732
+ case 'sync':
733
+ this.onListSync(ctx, opts)
734
+ break
564
735
  }
565
736
  }
566
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
+
567
779
  private onAwarenessUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
568
- 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)
569
786
  if (!awareness) {
570
787
  return
571
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>