@cuboapp/crdt 1.0.12 → 1.0.14
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 +16 -11
- package/src/client/index.ts +114 -22
- package/src/server/document/index.ts +1 -1
- package/src/server/index.ts +55 -35
- package/src/server/types/index.ts +9 -0
- package/.prettierrc +0 -8
- package/tsconfig.build.json +0 -4
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,30 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/crdt",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "CRDT for CUBO",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
|
-
"repository":
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git@cuboapp.gitlab.yandexcloud.net:cubo/crdt.git"
|
|
9
|
+
},
|
|
7
10
|
"author": "CuboSoft",
|
|
8
11
|
"license": "MIT",
|
|
9
12
|
"type": "module",
|
|
10
13
|
"publishConfig": {
|
|
11
14
|
"access": "public"
|
|
12
15
|
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
13
19
|
"dependencies": {
|
|
14
|
-
"@cuboapp/types": "^2.0.
|
|
15
|
-
"@cuboapp/utils": "1.0.
|
|
16
|
-
"@cuboapp/ws": "1.0.
|
|
17
|
-
"ws": "^8.19.0",
|
|
20
|
+
"@cuboapp/types": "^2.0.14",
|
|
21
|
+
"@cuboapp/utils": "1.0.12",
|
|
22
|
+
"@cuboapp/ws": "1.0.9",
|
|
18
23
|
"y-protocols": "^1.0.7",
|
|
19
|
-
"yjs": "^13.6.
|
|
24
|
+
"yjs": "^13.6.31"
|
|
20
25
|
},
|
|
21
26
|
"peerDependencies": {
|
|
22
|
-
"vue": "^3.5.
|
|
23
|
-
"vue-router": "^5.0
|
|
27
|
+
"vue": "^3.5.38",
|
|
28
|
+
"vue-router": "^5.1.0"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^
|
|
31
|
+
"@types/node": "^26.0.0",
|
|
27
32
|
"@types/ws": "^8.18.1",
|
|
28
|
-
"typescript": "^
|
|
33
|
+
"typescript": "^6.0.3"
|
|
29
34
|
}
|
|
30
35
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { keyBy, pick, uuid } from '@cuboapp/utils'
|
|
2
|
+
import { WsClientEvent } from '@cuboapp/ws'
|
|
2
3
|
import { computed, reactive } from 'vue'
|
|
3
|
-
import { applyUpdate, Doc } from 'yjs'
|
|
4
4
|
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
5
|
+
import { applyUpdate, Doc } from 'yjs'
|
|
5
6
|
|
|
6
7
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
7
8
|
import { CuboCrdtServerDocumentIncomingAction } from '../server'
|
|
8
9
|
import { CuboCrdtKey } from '../types'
|
|
9
10
|
|
|
11
|
+
import { AsyncSerialQueue } from './queue'
|
|
10
12
|
import {
|
|
11
13
|
CuboCrdtClientDocOrigin,
|
|
12
14
|
CuboCrdtClientDocUpdateOptions,
|
|
@@ -17,7 +19,6 @@ import {
|
|
|
17
19
|
CuboCrdtClientSubscribeEvent,
|
|
18
20
|
CuboCrdtClientUseOptions
|
|
19
21
|
} from './types'
|
|
20
|
-
import { AsyncSerialQueue } from './queue'
|
|
21
22
|
|
|
22
23
|
export * from './types'
|
|
23
24
|
|
|
@@ -25,6 +26,12 @@ export class CuboCrdtClient<M> {
|
|
|
25
26
|
public store: CuboCrdtClientStore<M> = {}
|
|
26
27
|
|
|
27
28
|
private listeners: Map<string, Map<string, (ctx: CuboCrdtClientSubscribeEvent) => void | Promise<void>>> = new Map()
|
|
29
|
+
|
|
30
|
+
// реестр активных подписок — нужен, чтобы переподписаться после реконнекта
|
|
31
|
+
private subscriptions = new Map<string, { entity: string; filters?: Record<string, any>; awareness?: boolean }>()
|
|
32
|
+
private connectedOnce = false
|
|
33
|
+
private onReconnect = () => this.resubscribeAll()
|
|
34
|
+
|
|
28
35
|
private eventQueue = new AsyncSerialQueue({
|
|
29
36
|
errorMode: 'continue',
|
|
30
37
|
onError: (e) => console.error('[CRDT] inbound queue task failed', e),
|
|
@@ -36,6 +43,13 @@ export class CuboCrdtClient<M> {
|
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
async start() {
|
|
46
|
+
// если сокет уже подключён к моменту старта — считаем текущий коннект «первым»,
|
|
47
|
+
// чтобы первое же событие CONNECTED (после обрыва) сработало как переподписка
|
|
48
|
+
this.connectedOnce = this.ws.connected
|
|
49
|
+
|
|
50
|
+
// переподписываемся на все документы при (ре)коннекте сокета
|
|
51
|
+
this.ws.ee.on(WsClientEvent.CONNECTED, this.onReconnect)
|
|
52
|
+
|
|
39
53
|
this.ws.registerHandler(CUBO_CRDT_EVENT.EVENT, ({ message }) => {
|
|
40
54
|
this.eventQueue
|
|
41
55
|
.enqueue(async () => {
|
|
@@ -68,9 +82,45 @@ export class CuboCrdtClient<M> {
|
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
async stop() {
|
|
85
|
+
this.ws.ee.off(WsClientEvent.CONNECTED, this.onReconnect)
|
|
86
|
+
|
|
71
87
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
|
|
72
88
|
|
|
73
89
|
this.listeners.clear()
|
|
90
|
+
this.subscriptions.clear()
|
|
91
|
+
this.connectedOnce = false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Переподписка на все активные подписки после реконнекта.
|
|
96
|
+
*
|
|
97
|
+
* При обрыве сокета бэкенд удаляет клиента вместе со всеми его подписками
|
|
98
|
+
* (removeClient -> cleanSubscribe), а после реконнекта сокету выдаётся новый id.
|
|
99
|
+
* Локальные слушатели (this.listeners) и yjs-документы при этом сохраняются,
|
|
100
|
+
* поэтому достаточно заново отправить SUBSCRIBE по реестру — заново
|
|
101
|
+
* навешивать слушатели не нужно.
|
|
102
|
+
*/
|
|
103
|
+
private resubscribeAll() {
|
|
104
|
+
// первый коннект — useList() уже сам отправил SUBSCRIBE, дублировать не нужно
|
|
105
|
+
if (!this.connectedOnce) {
|
|
106
|
+
this.connectedOnce = true
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.debug) {
|
|
111
|
+
console.log('[CRDT] reconnected, resubscribing', { count: this.subscriptions.size })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
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)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
74
124
|
}
|
|
75
125
|
|
|
76
126
|
public useComputedList<K extends Extract<keyof M, string>>(entity: K) {
|
|
@@ -138,8 +188,17 @@ export class CuboCrdtClient<M> {
|
|
|
138
188
|
})
|
|
139
189
|
})
|
|
140
190
|
|
|
141
|
-
//
|
|
142
|
-
this.
|
|
191
|
+
// регистрируем подписку, чтобы переподписаться после реконнекта
|
|
192
|
+
this.subscriptions.set(subscribe_id, { entity: `${entity}`, filters, awareness: opts?.awareness })
|
|
193
|
+
|
|
194
|
+
// подписываемся на бэке (если сокет ещё не подключён — переподпишемся по событию 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
|
+
}
|
|
201
|
+
}
|
|
143
202
|
}
|
|
144
203
|
|
|
145
204
|
// отписка
|
|
@@ -148,12 +207,23 @@ export class CuboCrdtClient<M> {
|
|
|
148
207
|
console.log('[CRDT] unsubscribe', { entity, opts, filters })
|
|
149
208
|
}
|
|
150
209
|
|
|
210
|
+
// снимаем подписку из реестра переподписки
|
|
211
|
+
this.subscriptions.delete(subscribe_id)
|
|
212
|
+
|
|
151
213
|
// отписываемся на бэке
|
|
152
|
-
|
|
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
|
+
}
|
|
220
|
+
}
|
|
153
221
|
|
|
154
222
|
// отписываемся на фронте
|
|
155
223
|
this.listeners.get(entity)?.delete(subscribe_id)
|
|
156
224
|
|
|
225
|
+
this.store[storeKey]!.state.subscribed = false
|
|
226
|
+
|
|
157
227
|
// очищаем стор
|
|
158
228
|
if (clear !== false) {
|
|
159
229
|
const item = this.store[storeKey]
|
|
@@ -279,18 +349,49 @@ export class CuboCrdtClient<M> {
|
|
|
279
349
|
}
|
|
280
350
|
|
|
281
351
|
public upgrade(subscribe_id: string, filters?: Record<string, any>) {
|
|
352
|
+
// держим реестр в актуальном состоянии, чтобы переподписка после реконнекта
|
|
353
|
+
// использовала уже обновлённые фильтры
|
|
354
|
+
const sub = this.subscriptions.get(subscribe_id)
|
|
355
|
+
if (sub) {
|
|
356
|
+
sub.filters = filters
|
|
357
|
+
}
|
|
358
|
+
|
|
282
359
|
this.ws.request({ method: CUBO_CRDT_EVENT.UPGRADE, data: { subscribe_id, filters } })
|
|
283
360
|
}
|
|
284
361
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
362
|
+
// апсертит строку в реактивное хранилище из текущего состояния yjs-документа
|
|
363
|
+
private applyRowJson(storeKey: string, entity_id: number, doc: Doc) {
|
|
364
|
+
const json: any = doc.getMap().toJSON()
|
|
365
|
+
const rows = this.store[storeKey]?.state.rows
|
|
366
|
+
if (!rows) {
|
|
289
367
|
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const index = rows.findIndex((r: any) => r.id === entity_id)
|
|
371
|
+
if (index >= 0) {
|
|
372
|
+
rows[index] = json
|
|
290
373
|
} else {
|
|
291
|
-
|
|
374
|
+
rows.push(json)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private async onDocumentCreate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
379
|
+
const existing = this.store[opts.storeKey]?.docs.get(ctx.entity_id)
|
|
380
|
+
if (existing) {
|
|
381
|
+
// документ уже есть локально (типичный кейс — повторный SUBSCRIBE после реконнекта).
|
|
382
|
+
// вместо игнорирования вмёрживаем входящее состояние в существующий yjs-документ
|
|
383
|
+
// (CRDT-merge), сохраняя локальные слушатели и awareness.
|
|
384
|
+
if (this.debug) {
|
|
385
|
+
console.log('[CRDT] onDocumentCreate: doc already exists, merging state', { ctx, opts })
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
applyUpdate(existing, new Uint8Array(ctx.data as any), { react: false })
|
|
389
|
+
this.applyRowJson(opts.storeKey, ctx.entity_id, existing)
|
|
390
|
+
return
|
|
292
391
|
}
|
|
293
392
|
|
|
393
|
+
const doc = new Doc()
|
|
394
|
+
|
|
294
395
|
const update = new Uint8Array(ctx.data as any)
|
|
295
396
|
|
|
296
397
|
if (opts?.onBeforeCreate) {
|
|
@@ -351,7 +452,8 @@ export class CuboCrdtClient<M> {
|
|
|
351
452
|
awareness = new Awareness(doc)
|
|
352
453
|
|
|
353
454
|
awareness.on('update', ({ added, updated, removed }, origin) => {
|
|
354
|
-
console.log('[CRDT] awarness update', origin)
|
|
455
|
+
// console.log('[CRDT] awarness update', origin)
|
|
456
|
+
|
|
355
457
|
if (origin === 'remote') {
|
|
356
458
|
return
|
|
357
459
|
}
|
|
@@ -396,17 +498,7 @@ export class CuboCrdtClient<M> {
|
|
|
396
498
|
applyUpdate(doc, update, { react: false })
|
|
397
499
|
|
|
398
500
|
// обновляем документ в реактивном хранилище
|
|
399
|
-
|
|
400
|
-
const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === ctx.entity_id)
|
|
401
|
-
if (index !== undefined && index >= 0) {
|
|
402
|
-
this.store[opts.storeKey]!.state.rows[index] = json
|
|
403
|
-
} else {
|
|
404
|
-
if (this.debug) {
|
|
405
|
-
console.warn('[CRDT] onDocumentUpdate - row not exists: "' + ctx.entity_id + '"', { ctx, opts })
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
this.store[opts.storeKey]!.state.rows.push(json)
|
|
409
|
-
}
|
|
501
|
+
this.applyRowJson(opts.storeKey, ctx.entity_id, doc)
|
|
410
502
|
|
|
411
503
|
if (opts?.onAfterUpdate) {
|
|
412
504
|
await opts.onAfterUpdate(doc, update, ctx, opts)
|
package/src/server/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cloneDeep } from '@cuboapp/utils'
|
|
2
2
|
import { WsServerSocket } from '@cuboapp/ws'
|
|
3
|
+
import { applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
3
4
|
|
|
4
5
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
5
6
|
import { CuboCrdtAction } from '../types'
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
CuboCrdtServerUnsubscribeDto,
|
|
17
18
|
CuboCrdtSocketClient
|
|
18
19
|
} from './types'
|
|
19
|
-
import { applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
20
20
|
|
|
21
21
|
export * from './document'
|
|
22
22
|
export * from './types'
|
|
@@ -99,7 +99,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
99
99
|
console.log('[CRDT] incoming event', data)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
for (const row of data) {
|
|
103
103
|
const update = new Uint8Array(row.data)
|
|
104
104
|
|
|
105
105
|
switch (row.action) {
|
|
@@ -114,10 +114,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
114
114
|
break
|
|
115
115
|
case 'awareness':
|
|
116
116
|
this.onAwarenessExternalUpdate(update, row)
|
|
117
|
-
|
|
118
117
|
break
|
|
119
118
|
}
|
|
120
|
-
}
|
|
119
|
+
}
|
|
121
120
|
})
|
|
122
121
|
|
|
123
122
|
this.ws.registerHttpHandler('GET', '/stats', () => {
|
|
@@ -170,6 +169,10 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
170
169
|
// удаляем мапу подписок по клиенту
|
|
171
170
|
this.subscribesByClient.delete(client.id)
|
|
172
171
|
|
|
172
|
+
// снимаем запланированный флаш очереди (чтобы не слать в закрытый сокет)
|
|
173
|
+
clearTimeout(this.sendTimeouts.get(client.id))
|
|
174
|
+
this.sendTimeouts.delete(client.id)
|
|
175
|
+
|
|
173
176
|
// удаляем очередь под клиента
|
|
174
177
|
this.queue.delete(client.id)
|
|
175
178
|
|
|
@@ -177,14 +180,26 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
177
180
|
this.clients.delete(client.id)
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
private get batchMaxSize() {
|
|
184
|
+
return this.options.batch?.maxSize ?? 50
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private get batchDebounce() {
|
|
188
|
+
return this.options.batch?.debounce ?? 100
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
private sendQueueToClient(client: CuboCrdtSocketClient<A>) {
|
|
192
|
+
// снимаем запланированный флаш — мы отправляем прямо сейчас
|
|
193
|
+
clearTimeout(this.sendTimeouts.get(client.id))
|
|
194
|
+
this.sendTimeouts.delete(client.id)
|
|
195
|
+
|
|
181
196
|
const queue = this.queue.get(client.id)
|
|
182
|
-
if (!queue) {
|
|
197
|
+
if (!queue || queue.size === 0) {
|
|
183
198
|
return
|
|
184
199
|
}
|
|
185
200
|
|
|
186
|
-
//
|
|
187
|
-
const data = [...new Map(
|
|
201
|
+
// дедупликация одинаковых событий внутри батча
|
|
202
|
+
const data = [...new Map(Array.from(queue).map((item) => [JSON.stringify(item), item])).values()]
|
|
188
203
|
|
|
189
204
|
queue.clear()
|
|
190
205
|
|
|
@@ -194,8 +209,6 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
194
209
|
data
|
|
195
210
|
})
|
|
196
211
|
)
|
|
197
|
-
|
|
198
|
-
queue.clear()
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
public sendToClient(client_id: string, data: any) {
|
|
@@ -204,21 +217,26 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
204
217
|
return
|
|
205
218
|
}
|
|
206
219
|
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
const queue = this.queue.get(client_id)
|
|
221
|
+
if (!queue) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
209
224
|
|
|
210
225
|
// добавляем в очередь
|
|
211
|
-
|
|
226
|
+
queue.add(data)
|
|
212
227
|
|
|
213
|
-
// если
|
|
214
|
-
if (
|
|
228
|
+
// если у ЭТОГО клиента в очереди накопилось много событий — отправляем сразу
|
|
229
|
+
if (queue.size >= this.batchMaxSize) {
|
|
215
230
|
this.sendQueueToClient(client)
|
|
231
|
+
return
|
|
216
232
|
}
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
|
|
234
|
+
// иначе — дебаунсим отправку, но НЕ сбрасываем уже запланированный флаш,
|
|
235
|
+
// иначе при непрерывном потоке апдейтов очередь никогда не отправится
|
|
236
|
+
if (!this.sendTimeouts.has(client_id)) {
|
|
219
237
|
this.sendTimeouts.set(
|
|
220
238
|
client_id,
|
|
221
|
-
setTimeout(() => this.sendQueueToClient(client),
|
|
239
|
+
setTimeout(() => this.sendQueueToClient(client), this.batchDebounce)
|
|
222
240
|
)
|
|
223
241
|
}
|
|
224
242
|
}
|
|
@@ -288,18 +306,19 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
288
306
|
}
|
|
289
307
|
|
|
290
308
|
public getSutableSubscribes(entity: string, row: any) {
|
|
291
|
-
return Array.from(this.subscribesByEntity.get(entity as E) || [])
|
|
292
|
-
.
|
|
293
|
-
|
|
309
|
+
return Array.from(this.subscribesByEntity.get(entity as E) || []).filter((subscribe_id) => {
|
|
310
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
311
|
+
if (!subscribe) {
|
|
312
|
+
return false
|
|
313
|
+
}
|
|
294
314
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
315
|
+
let state = checkRowIsSutable(row, subscribe.filters || {})
|
|
316
|
+
if (this.options.checkRowIsSutable) {
|
|
317
|
+
state = this.options.checkRowIsSutable(state, { entity: entity as any, subscribe, row })
|
|
318
|
+
}
|
|
299
319
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
.map((m) => m[1])
|
|
320
|
+
return state
|
|
321
|
+
})
|
|
303
322
|
}
|
|
304
323
|
|
|
305
324
|
public ensureDocument(entity: E, row: any, awareness?: boolean) {
|
|
@@ -381,15 +400,16 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
381
400
|
}
|
|
382
401
|
|
|
383
402
|
private onDocumentExternalUpdate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// console.log('onDocumentExternalUpdate', action)
|
|
387
|
-
|
|
403
|
+
let document = this.getDocument(action.entity, action.entity_id)
|
|
388
404
|
if (!document) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
405
|
+
if (this.debug) {
|
|
406
|
+
console.warn('[CRDT] onDocumentExternalUpdate - document not found, creating:', action.entity, action.entity_id)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
document = this.ensureDocument(action.entity as E, action.entity_id, true)
|
|
392
410
|
}
|
|
411
|
+
|
|
412
|
+
document.applyUpdate(new Uint8Array(update), action.origin)
|
|
393
413
|
}
|
|
394
414
|
|
|
395
415
|
private onAwarenessExternalUpdate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
|
|
@@ -445,7 +465,7 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
445
465
|
const row = document.getJson()
|
|
446
466
|
|
|
447
467
|
// берём все подписки по сущности
|
|
448
|
-
const sutableSubscribes = Array.from(this.subscribesByEntity.get(entity))
|
|
468
|
+
const sutableSubscribes = Array.from(this.subscribesByEntity.get(entity) || [])
|
|
449
469
|
.map((subscribe_id) => {
|
|
450
470
|
const subscribe = this.subscribes.get(subscribe_id)
|
|
451
471
|
|
|
@@ -10,6 +10,15 @@ export type CuboCrdtServerOptions<M, A, E extends Extract<keyof M, string>> = {
|
|
|
10
10
|
ws: WsServer
|
|
11
11
|
entities: E[]
|
|
12
12
|
debug?: boolean
|
|
13
|
+
/**
|
|
14
|
+
* Параметры батчинга исходящих событий (защита от перегрузки бэка).
|
|
15
|
+
* - maxSize: при достижении такого размера очереди клиента батч отправляется сразу
|
|
16
|
+
* - debounce: задержка перед отправкой неполного батча (мс)
|
|
17
|
+
*/
|
|
18
|
+
batch?: {
|
|
19
|
+
maxSize?: number
|
|
20
|
+
debounce?: number
|
|
21
|
+
}
|
|
13
22
|
fetchRows?: (client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) => Promise<M[E][]>
|
|
14
23
|
checkRowIsSutable?: (baseState: boolean, ctx: { entity: E; subscribe: CuboCrdtServerSubscribe; row: any }) => boolean
|
|
15
24
|
storeRow?: <K extends E>(
|
package/.prettierrc
DELETED
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "commonjs",
|
|
4
|
-
"declaration": true,
|
|
5
|
-
"removeComments": true,
|
|
6
|
-
"emitDecoratorMetadata": true,
|
|
7
|
-
"experimentalDecorators": true,
|
|
8
|
-
"allowSyntheticDefaultImports": true,
|
|
9
|
-
"target": "ES2021",
|
|
10
|
-
"sourceMap": true,
|
|
11
|
-
"outDir": "./dist",
|
|
12
|
-
"baseUrl": "./",
|
|
13
|
-
"incremental": true,
|
|
14
|
-
"skipLibCheck": true,
|
|
15
|
-
"paths": {
|
|
16
|
-
"@/*": ["src/*"]
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*"],
|
|
20
|
-
"exclude": ["node_modules", "dist"]
|
|
21
|
-
}
|