@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 +2 -2
- package/src/client/index.ts +275 -58
- package/src/client/types/index.ts +19 -3
- package/src/client/types/store.ts +10 -1
- package/src/server/document/index.ts +29 -4
- package/src/server/index.ts +539 -160
- package/src/server/types/document.ts +10 -1
- package/src/server/types/index.ts +20 -1
- package/src/server/types/subscribe.ts +15 -2
- package/src/types/index.ts +24 -1
- package/src/utils/index.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/crdt",
|
|
3
|
-
"version": "1.0.
|
|
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.
|
|
22
|
+
"@cuboapp/ws": "1.0.10",
|
|
23
23
|
"y-protocols": "^1.0.7",
|
|
24
24
|
"yjs": "^13.6.31"
|
|
25
25
|
},
|
package/src/client/index.ts
CHANGED
|
@@ -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 {
|
|
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<
|
|
32
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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.
|
|
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, {
|
|
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
|
-
|
|
196
|
-
this.ws
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
215
|
-
this.ws
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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 ===
|
|
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: "' +
|
|
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(
|
|
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
|
|
626
|
+
entity_id,
|
|
470
627
|
data: Array.from(update),
|
|
471
|
-
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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>
|