@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 +2 -2
- package/src/client/index.ts +269 -57
- package/src/client/types/index.ts +19 -3
- package/src/client/types/store.ts +10 -1
- package/src/server/document/index.ts +18 -3
- package/src/server/index.ts +374 -82
- 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.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.
|
|
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,7 +563,7 @@ 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: {
|
|
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(
|
|
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 ===
|
|
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: "' +
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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
|
|
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 {
|
|
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>
|
|
@@ -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.
|
|
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
|
}
|