@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/src/server/index.ts
CHANGED
|
@@ -3,8 +3,12 @@ import { WsServerSocket } from '@cuboapp/ws'
|
|
|
3
3
|
import { applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
|
|
4
4
|
|
|
5
5
|
import { CUBO_CRDT_EVENT } from '../constants'
|
|
6
|
-
import { CuboCrdtAction } from '../types'
|
|
7
|
-
import {
|
|
6
|
+
import { CuboCrdtAction, CuboCrdtListSyncData, CuboCrdtMutation } from '../types'
|
|
7
|
+
import {
|
|
8
|
+
areCrdtListIdsEqual,
|
|
9
|
+
areCrdtListTotalsEqual,
|
|
10
|
+
checkRowIsSutable
|
|
11
|
+
} from '../utils'
|
|
8
12
|
|
|
9
13
|
import { CuboCrdtClientDocOrigin } from '../client'
|
|
10
14
|
import { CuboCrdtServerDocument } from './document'
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
CuboCrdtServerOptions,
|
|
15
19
|
CuboCrdtServerSubscribe,
|
|
16
20
|
CuboCrdtServerSubscribeDto,
|
|
21
|
+
CuboCrdtSubscribeRefreshState,
|
|
17
22
|
CuboCrdtServerUnsubscribeDto,
|
|
18
23
|
CuboCrdtSocketClient
|
|
19
24
|
} from './types'
|
|
@@ -31,28 +36,40 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
31
36
|
private subscribes = new Map<string, CuboCrdtServerSubscribe>()
|
|
32
37
|
private subscribesByClient = new Map<string, Set<string>>()
|
|
33
38
|
private subscribesByEntity = new Map<E, Set<string>>()
|
|
34
|
-
private
|
|
35
|
-
private
|
|
39
|
+
private subscribeRefreshStates = new Map<string, CuboCrdtSubscribeRefreshState>()
|
|
40
|
+
private subscribeGenerations = new Map<string, number>()
|
|
36
41
|
|
|
37
42
|
private documents = new Map<string, CuboCrdtServerDocument>()
|
|
38
43
|
private subscribesByDocument = new Map<string, Set<string>>()
|
|
44
|
+
// обратный индекс: подписка -> имена документов, к которым она привязана.
|
|
45
|
+
// Нужен, чтобы отписка/очистка не сканировала ВСЕ документы сервера (O(subs x docs)).
|
|
46
|
+
private documentsBySubscribe = new Map<string, Set<string>>()
|
|
39
47
|
|
|
40
48
|
public async init() {
|
|
41
49
|
this.entities.forEach((e) => this.subscribesByEntity.set(e, new Set()))
|
|
42
50
|
|
|
43
51
|
this.ws.registerHandler(CUBO_CRDT_EVENT.SUBSCRIBE, async ({ client, message }) => {
|
|
44
|
-
const { subscribe_id: id, entity, filters, awareness = false } = message.data as CuboCrdtServerSubscribeDto
|
|
52
|
+
const { subscribe_id: id, entity, filters, awareness = false, paginated } = message.data as CuboCrdtServerSubscribeDto
|
|
45
53
|
|
|
46
54
|
if (this.debug) {
|
|
47
55
|
console.log('[CRDT] subscribe', entity, filters)
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
const existing = this.subscribes.get(id)
|
|
59
|
+
if (existing) {
|
|
60
|
+
this.cleanSubscribe(id)
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
const subscribe = {
|
|
51
64
|
id,
|
|
52
65
|
client_id: client.id,
|
|
53
66
|
entity,
|
|
54
|
-
filters,
|
|
55
|
-
awareness
|
|
67
|
+
filters: cloneDeep(filters || {}),
|
|
68
|
+
awareness,
|
|
69
|
+
paginated: this.resolvePaginated(filters, paginated),
|
|
70
|
+
row_ids: [],
|
|
71
|
+
totals: {},
|
|
72
|
+
revision: 0
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
this.subscribes.set(id, subscribe)
|
|
@@ -64,11 +81,13 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
64
81
|
this.subscribesByEntity.get(entity as E)?.add(id)
|
|
65
82
|
|
|
66
83
|
// инициализируем подписку
|
|
67
|
-
this.initSubscribe(client, subscribe)
|
|
84
|
+
await this.initSubscribe(client, subscribe)
|
|
85
|
+
|
|
86
|
+
return { subscribe_id: id, revision: subscribe.revision }
|
|
68
87
|
})
|
|
69
88
|
|
|
70
89
|
this.ws.registerHandler(CUBO_CRDT_EVENT.UPGRADE, async ({ client, message }) => {
|
|
71
|
-
const { subscribe_id: id, entity, filters } = message.data as CuboCrdtServerSubscribeDto
|
|
90
|
+
const { subscribe_id: id, entity, filters, paginated } = message.data as CuboCrdtServerSubscribeDto
|
|
72
91
|
|
|
73
92
|
if (this.debug) {
|
|
74
93
|
console.log('[CRDT] upgrade', entity, filters)
|
|
@@ -80,7 +99,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
// обновляем подписку
|
|
83
|
-
this.upgradeSubscribe(client, subscribe, { filters })
|
|
102
|
+
await this.upgradeSubscribe(client, subscribe, { filters, paginated })
|
|
103
|
+
|
|
104
|
+
return { subscribe_id: id, revision: subscribe.revision }
|
|
84
105
|
})
|
|
85
106
|
|
|
86
107
|
this.ws.registerHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE, async ({ message }) => {
|
|
@@ -145,9 +166,14 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
145
166
|
|
|
146
167
|
public async destroy() {
|
|
147
168
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.SUBSCRIBE)
|
|
169
|
+
this.ws.deleteHandler(CUBO_CRDT_EVENT.UPGRADE)
|
|
148
170
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE)
|
|
149
171
|
this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
|
|
150
172
|
|
|
173
|
+
this.subscribeRefreshStates.forEach((state) => clearTimeout(state.timer))
|
|
174
|
+
this.subscribeRefreshStates.clear()
|
|
175
|
+
this.subscribeGenerations.clear()
|
|
176
|
+
|
|
151
177
|
this.subscribes.forEach((sub) => this.ws.deleteHandler(sub.id))
|
|
152
178
|
}
|
|
153
179
|
|
|
@@ -163,8 +189,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
163
189
|
}
|
|
164
190
|
|
|
165
191
|
public removeClient(client: WsServerSocket) {
|
|
166
|
-
// удаляем все подписки по клиенту
|
|
167
|
-
|
|
192
|
+
// удаляем все подписки по клиенту (но только те, что всё ещё принадлежат ЭТОМУ сокету —
|
|
193
|
+
// подписка могла быть переподвязана к новому сокету при reconnect с тем же subscribe_id)
|
|
194
|
+
this.subscribesByClient.get(client.id)?.forEach((subscribe_id) => this.cleanSubscribe(subscribe_id, client.id))
|
|
168
195
|
|
|
169
196
|
// удаляем мапу подписок по клиенту
|
|
170
197
|
this.subscribesByClient.delete(client.id)
|
|
@@ -188,6 +215,21 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
188
215
|
return this.options.batch?.debounce ?? 100
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
private get paginationDebounce() {
|
|
219
|
+
return this.options.pagination?.debounce ?? 75
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private resolvePaginated(filters?: Record<string, any>, explicit?: boolean) {
|
|
223
|
+
if (explicit !== undefined) {
|
|
224
|
+
return explicit
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const page = Number(filters?.page)
|
|
228
|
+
const limit = Number(filters?.limit)
|
|
229
|
+
|
|
230
|
+
return Number.isFinite(page) && page > 0 && Number.isFinite(limit) && limit > 0
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
private sendQueueToClient(client: CuboCrdtSocketClient<A>) {
|
|
192
234
|
// снимаем запланированный флаш — мы отправляем прямо сейчас
|
|
193
235
|
clearTimeout(this.sendTimeouts.get(client.id))
|
|
@@ -198,17 +240,17 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
198
240
|
return
|
|
199
241
|
}
|
|
200
242
|
|
|
201
|
-
//
|
|
202
|
-
|
|
243
|
+
// Дедупликация побайтово идентичных событий внутри батча + сборка кадра за ОДИН проход
|
|
244
|
+
// сериализации: сериализуем каждый элемент один раз (он же служит ключом дедупа) и
|
|
245
|
+
// склеиваем готовые JSON-строки, а не сериализуем весь массив повторно.
|
|
246
|
+
const items = new Set<string>()
|
|
247
|
+
for (const item of queue) {
|
|
248
|
+
items.add(JSON.stringify(item))
|
|
249
|
+
}
|
|
203
250
|
|
|
204
251
|
queue.clear()
|
|
205
252
|
|
|
206
|
-
client.send(
|
|
207
|
-
JSON.stringify({
|
|
208
|
-
method: CUBO_CRDT_EVENT.EVENT,
|
|
209
|
-
data
|
|
210
|
-
})
|
|
211
|
-
)
|
|
253
|
+
client.send(`{"method":${JSON.stringify(CUBO_CRDT_EVENT.EVENT)},"data":[${Array.from(items).join(',')}]}`)
|
|
212
254
|
}
|
|
213
255
|
|
|
214
256
|
public sendToClient(client_id: string, data: any) {
|
|
@@ -241,6 +283,29 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
241
283
|
}
|
|
242
284
|
}
|
|
243
285
|
|
|
286
|
+
// привязываем подписку к документу (в обоих индексах)
|
|
287
|
+
private linkSubscribeToDocument(subscribe_id: string, docName: string) {
|
|
288
|
+
let byDoc = this.subscribesByDocument.get(docName)
|
|
289
|
+
if (!byDoc) {
|
|
290
|
+
byDoc = new Set()
|
|
291
|
+
this.subscribesByDocument.set(docName, byDoc)
|
|
292
|
+
}
|
|
293
|
+
byDoc.add(subscribe_id)
|
|
294
|
+
|
|
295
|
+
let bySub = this.documentsBySubscribe.get(subscribe_id)
|
|
296
|
+
if (!bySub) {
|
|
297
|
+
bySub = new Set()
|
|
298
|
+
this.documentsBySubscribe.set(subscribe_id, bySub)
|
|
299
|
+
}
|
|
300
|
+
bySub.add(docName)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// отвязываем подписку от документа (в обоих индексах)
|
|
304
|
+
private unlinkSubscribeFromDocument(subscribe_id: string, docName: string) {
|
|
305
|
+
this.subscribesByDocument.get(docName)?.delete(subscribe_id)
|
|
306
|
+
this.documentsBySubscribe.get(subscribe_id)?.delete(docName)
|
|
307
|
+
}
|
|
308
|
+
|
|
244
309
|
// если у документа нет подписок - сносим его
|
|
245
310
|
private checkDocumentNeedRemove(name: string) {
|
|
246
311
|
if (!this.subscribesByDocument.get(name)?.size) {
|
|
@@ -250,55 +315,67 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
250
315
|
|
|
251
316
|
this.documents.get(name)?.destroy()
|
|
252
317
|
this.documents.delete(name)
|
|
318
|
+
// не оставляем пустой Set в индексе (иначе — медленная утечка записей мапы);
|
|
319
|
+
// getOrCreateDocument пересоздаст его при повторном появлении документа
|
|
320
|
+
this.subscribesByDocument.delete(name)
|
|
253
321
|
}
|
|
254
322
|
}
|
|
255
323
|
|
|
256
|
-
// очистка подписки (при отписке клиента - через emit-метод или options-хук)
|
|
257
|
-
|
|
324
|
+
// очистка подписки (при отписке клиента - через emit-метод или options-хук).
|
|
325
|
+
// ownerClientId (опционально): при отключении сокета передаётся id этого сокета —
|
|
326
|
+
// если подписка уже переподвязана к НОВОМУ клиенту (reconnect с тем же subscribe_id),
|
|
327
|
+
// старый close-хендлер не должен её сносить.
|
|
328
|
+
private cleanSubscribe(subscribe_id: string, ownerClientId?: string) {
|
|
258
329
|
const subscribe = this.subscribes.get(subscribe_id)
|
|
259
|
-
if (subscribe) {
|
|
330
|
+
if (!subscribe) {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// подписка была переподвязана к другому (более новому) клиенту — не трогаем
|
|
335
|
+
if (ownerClientId !== undefined && subscribe.client_id !== ownerClientId) {
|
|
260
336
|
if (this.debug) {
|
|
261
|
-
console.log('[CRDT]
|
|
337
|
+
console.log('[CRDT] cleanSubscribe skipped (rebound to newer client)', subscribe_id)
|
|
262
338
|
}
|
|
339
|
+
return
|
|
340
|
+
}
|
|
263
341
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!subs.has(subscribe_id)) {
|
|
268
|
-
return
|
|
269
|
-
}
|
|
342
|
+
if (this.debug) {
|
|
343
|
+
console.log('[CRDT] unsubscribe', subscribe.entity, subscribe.filters)
|
|
344
|
+
}
|
|
270
345
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return
|
|
274
|
-
}
|
|
346
|
+
// документы этой подписки берём из обратного индекса, а не сканируем все
|
|
347
|
+
const docNames = this.documentsBySubscribe.get(subscribe_id)
|
|
275
348
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
349
|
+
if (docNames) {
|
|
350
|
+
for (const docName of docNames) {
|
|
351
|
+
if (subscribe.awareness) {
|
|
352
|
+
//убираем аварнесс стейт при отписке
|
|
353
|
+
const doc = this.documents.get(docName)
|
|
354
|
+
const ids = doc?.awareness ? Array.from(doc.awarenessBySubscribe.get(subscribe_id) || []) : []
|
|
355
|
+
if (doc?.awareness && ids.length) {
|
|
356
|
+
removeAwarenessStates(doc.awareness, ids, { subscribe_id, expose: 'all' })
|
|
357
|
+
doc.awarenessBySubscribe.delete(subscribe_id)
|
|
279
358
|
}
|
|
359
|
+
}
|
|
280
360
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
})
|
|
361
|
+
// отписываем документ от подписки (только по одной стороне индекса — другую чистим ниже)
|
|
362
|
+
this.subscribesByDocument.get(docName)?.delete(subscribe_id)
|
|
363
|
+
this.checkDocumentNeedRemove(docName)
|
|
285
364
|
}
|
|
365
|
+
}
|
|
286
366
|
|
|
287
|
-
|
|
288
|
-
|
|
367
|
+
// удаляем обратный индекс
|
|
368
|
+
this.documentsBySubscribe.delete(subscribe_id)
|
|
289
369
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
370
|
+
const refreshState = this.subscribeRefreshStates.get(subscribe_id)
|
|
371
|
+
clearTimeout(refreshState?.timer)
|
|
372
|
+
this.subscribeRefreshStates.delete(subscribe_id)
|
|
373
|
+
this.subscribeGenerations.delete(subscribe_id)
|
|
293
374
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.subscribes.delete(subscribe_id)
|
|
299
|
-
this.subscribesByClient.get(subscribe.client_id)?.delete(subscribe_id)
|
|
300
|
-
this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe_id)
|
|
301
|
-
}
|
|
375
|
+
// удаляем подписки
|
|
376
|
+
this.subscribes.delete(subscribe_id)
|
|
377
|
+
this.subscribesByClient.get(subscribe.client_id)?.delete(subscribe_id)
|
|
378
|
+
this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe_id)
|
|
302
379
|
}
|
|
303
380
|
|
|
304
381
|
public getDocument(entity: string, entity_id: number) {
|
|
@@ -321,60 +398,298 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
321
398
|
})
|
|
322
399
|
}
|
|
323
400
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
401
|
+
private getSubscribeRefreshState(subscribe_id: string) {
|
|
402
|
+
let state = this.subscribeRefreshStates.get(subscribe_id)
|
|
403
|
+
if (!state) {
|
|
404
|
+
state = { dirty: false, forceSync: false }
|
|
405
|
+
this.subscribeRefreshStates.set(subscribe_id, state)
|
|
406
|
+
}
|
|
407
|
+
return state
|
|
408
|
+
}
|
|
328
409
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
initialState: row,
|
|
335
|
-
awareness: awareness,
|
|
336
|
-
onUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
337
|
-
// пушим обновление документа
|
|
338
|
-
// console.log('push document update', document.name)
|
|
339
|
-
|
|
340
|
-
this.pushDocumentAction('update', entity, document!, data, origin)
|
|
341
|
-
},
|
|
342
|
-
onAwarenessUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
343
|
-
// пушим обновление документа
|
|
344
|
-
// console.log('push document update', document.name)
|
|
410
|
+
private bumpSubscribeGeneration(subscribe_id: string) {
|
|
411
|
+
const generation = (this.subscribeGenerations.get(subscribe_id) || 0) + 1
|
|
412
|
+
this.subscribeGenerations.set(subscribe_id, generation)
|
|
413
|
+
return generation
|
|
414
|
+
}
|
|
345
415
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
416
|
+
private runSubscribeRefresh(subscribe_id: string) {
|
|
417
|
+
const state = this.getSubscribeRefreshState(subscribe_id)
|
|
418
|
+
if (state.running) {
|
|
419
|
+
return state.running
|
|
420
|
+
}
|
|
351
421
|
|
|
352
|
-
|
|
353
|
-
|
|
422
|
+
state.running = (async () => {
|
|
423
|
+
while (state.dirty) {
|
|
424
|
+
state.dirty = false
|
|
425
|
+
|
|
426
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
427
|
+
if (!subscribe) {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const client = this.clients.get(subscribe.client_id)
|
|
432
|
+
if (!client) {
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const generation = this.subscribeGenerations.get(subscribe_id) || 0
|
|
437
|
+
const forceSync = state.forceSync
|
|
438
|
+
state.forceSync = false
|
|
439
|
+
const reconciled = await this.reconcileSubscribe(
|
|
440
|
+
client,
|
|
441
|
+
subscribe,
|
|
442
|
+
generation,
|
|
443
|
+
forceSync
|
|
444
|
+
)
|
|
445
|
+
if (!reconciled && forceSync) {
|
|
446
|
+
state.forceSync = true
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
})().finally(() => {
|
|
450
|
+
state.running = undefined
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return state.running
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private requestSubscribeRefresh(
|
|
457
|
+
subscribe_id: string,
|
|
458
|
+
immediate = false,
|
|
459
|
+
forceSync = immediate
|
|
460
|
+
) {
|
|
461
|
+
const state = this.getSubscribeRefreshState(subscribe_id)
|
|
462
|
+
state.dirty = true
|
|
463
|
+
state.forceSync ||= forceSync
|
|
464
|
+
this.bumpSubscribeGeneration(subscribe_id)
|
|
465
|
+
|
|
466
|
+
if (immediate) {
|
|
467
|
+
clearTimeout(state.timer)
|
|
468
|
+
state.timer = undefined
|
|
469
|
+
return this.runSubscribeRefresh(subscribe_id)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!state.timer && !state.running) {
|
|
473
|
+
state.timer = setTimeout(() => {
|
|
474
|
+
state.timer = undefined
|
|
475
|
+
this.runSubscribeRefresh(subscribe_id).catch((e) => {
|
|
476
|
+
if (this.debug) {
|
|
477
|
+
console.error('[CRDT] refresh subscribe', subscribe_id, e)
|
|
354
478
|
}
|
|
479
|
+
})
|
|
480
|
+
}, this.paginationDebounce)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return state.running ?? Promise.resolve()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private getSubscribeQueryKeys(filters?: Record<string, any>) {
|
|
487
|
+
const keys = new Set<string>()
|
|
488
|
+
|
|
489
|
+
Object.keys(filters || {}).forEach((key) => {
|
|
490
|
+
if (!['page', 'limit', 'sort', 'with'].includes(key)) {
|
|
491
|
+
keys.add(key)
|
|
492
|
+
|
|
493
|
+
const rangeKey = key.replace(/_(start|end)$/, '')
|
|
494
|
+
if (rangeKey !== key) {
|
|
495
|
+
keys.add(rangeKey)
|
|
355
496
|
}
|
|
356
|
-
})
|
|
357
497
|
|
|
358
|
-
|
|
359
|
-
|
|
498
|
+
if (key.endsWith('_ids')) {
|
|
499
|
+
keys.add(key.slice(0, -1))
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
const appendSort = (sort: unknown) => {
|
|
505
|
+
if (Array.isArray(sort)) {
|
|
506
|
+
sort.forEach(appendSort)
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
if (typeof sort !== 'string') {
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
sort
|
|
514
|
+
.split(',')
|
|
515
|
+
.map((item) => item.trim().replace(/^-/, '').split(':')[0])
|
|
516
|
+
.filter(Boolean)
|
|
517
|
+
.forEach((key) => keys.add(key))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
appendSort(filters?.sort)
|
|
521
|
+
|
|
522
|
+
return keys
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private shouldRefreshSubscribe(entity: E, subscribe: CuboCrdtServerSubscribe, mutation: CuboCrdtMutation<M[E]>) {
|
|
526
|
+
if (!subscribe.paginated) {
|
|
527
|
+
return false
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (this.options.shouldRefreshSubscribe) {
|
|
531
|
+
return this.options.shouldRefreshSubscribe({ entity, subscribe, mutation })
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (mutation.action !== 'update' || !mutation.changedKeys?.length) {
|
|
535
|
+
return true
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (mutation.previousRow) {
|
|
539
|
+
const previousSuitable = this.checkRowIsSutable(
|
|
540
|
+
mutation.previousRow,
|
|
541
|
+
subscribe,
|
|
542
|
+
entity
|
|
543
|
+
)
|
|
544
|
+
const currentSuitable = this.checkRowIsSutable(
|
|
545
|
+
mutation.row,
|
|
546
|
+
subscribe,
|
|
547
|
+
entity
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if (previousSuitable !== currentSuitable) {
|
|
551
|
+
return true
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (subscribe.filters?.search) {
|
|
556
|
+
return true
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const queryKeys = this.getSubscribeQueryKeys(subscribe.filters)
|
|
560
|
+
return mutation.changedKeys.some((key) => queryKeys.has(key))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Уведомляет пагинированные подписки о сохранённой мутации.
|
|
565
|
+
* Пересчитываются только реально открытые страницы; несколько мутаций
|
|
566
|
+
* схлопываются debounce-ом в один запрос на подписку.
|
|
567
|
+
*/
|
|
568
|
+
public notifyMutation(entity: E, mutation: CuboCrdtMutation<M[E]>) {
|
|
569
|
+
for (const subscribe_id of this.subscribesByEntity.get(entity) || []) {
|
|
570
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
571
|
+
if (subscribe && this.shouldRefreshSubscribe(entity, subscribe, mutation)) {
|
|
572
|
+
this.requestSubscribeRefresh(subscribe_id)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Принудительно инвалидирует все открытые пагинированные окна сущности.
|
|
579
|
+
* Используется для вычисляемых полей и зависимых сущностей, когда changedKeys
|
|
580
|
+
* исходной строки недостаточно для определения влияния на запрос.
|
|
581
|
+
*/
|
|
582
|
+
public invalidate(entity: E) {
|
|
583
|
+
for (const subscribe_id of this.subscribesByEntity.get(entity) || []) {
|
|
584
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
585
|
+
if (subscribe?.paginated) {
|
|
586
|
+
this.requestSubscribeRefresh(subscribe_id)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// создаёт (или возвращает существующий) серверный документ БЕЗ рассылки подписчикам.
|
|
592
|
+
// Используется как для рассылочного пути (ensureDocument), так и для точечной
|
|
593
|
+
// доставки на init подписки (pushSubscribeDocuments).
|
|
594
|
+
private getOrCreateDocument(entity: E, row: any, awareness?: boolean): CuboCrdtServerDocument {
|
|
595
|
+
const entity_id = row.id
|
|
596
|
+
const documentName = `${entity}:${entity_id}`
|
|
597
|
+
|
|
598
|
+
let document = this.documents.get(documentName)
|
|
599
|
+
if (document) {
|
|
600
|
+
return document
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
document = new CuboCrdtServerDocument({
|
|
604
|
+
name: documentName,
|
|
605
|
+
initialState: row,
|
|
606
|
+
awareness: awareness,
|
|
607
|
+
onUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
608
|
+
this.pushDocumentAction('update', entity, document!, data, origin)
|
|
609
|
+
},
|
|
610
|
+
onAwarenessUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
611
|
+
this.pushDocumentAction('awareness', entity, document!, data, origin)
|
|
612
|
+
},
|
|
613
|
+
onStore: (item: object, origin: CuboCrdtServerDocumentOrigin, store) => {
|
|
614
|
+
const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
|
|
615
|
+
const client = subscribe && this.clients.get(subscribe.client_id)
|
|
616
|
+
|
|
617
|
+
if (client) {
|
|
618
|
+
return this.options?.storeRow?.(entity as any, entity_id, item as any, {
|
|
619
|
+
document,
|
|
620
|
+
client,
|
|
621
|
+
origin,
|
|
622
|
+
previousRow: store.previousRow as any,
|
|
623
|
+
row: store.row as any
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
})
|
|
360
628
|
|
|
361
|
-
|
|
629
|
+
this.documents.set(documentName, document)
|
|
630
|
+
if (!this.subscribesByDocument.has(documentName)) {
|
|
362
631
|
this.subscribesByDocument.set(documentName, new Set())
|
|
632
|
+
}
|
|
363
633
|
|
|
364
|
-
|
|
365
|
-
|
|
634
|
+
return document
|
|
635
|
+
}
|
|
366
636
|
|
|
367
|
-
|
|
637
|
+
// Рассылочный путь (хук onAfterCreate/onAfterUpdate из бэка): создаёт документ и
|
|
638
|
+
// раскатывает 'create'/'upsert' по ВСЕМ подходящим подпискам сущности.
|
|
639
|
+
public ensureDocument(entity: E, row: any, awareness?: boolean) {
|
|
640
|
+
if (row?.id == null) {
|
|
641
|
+
if (this.debug) {
|
|
642
|
+
console.warn('[CRDT] ensureDocument: row without id', entity, row)
|
|
643
|
+
}
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const documentName = `${entity}:${row.id}`
|
|
648
|
+
const existed = this.documents.has(documentName)
|
|
649
|
+
const document = this.getOrCreateDocument(entity, row, awareness)
|
|
650
|
+
|
|
651
|
+
if (!existed) {
|
|
652
|
+
this.pushDocumentAction('create', entity, document, document.stateAsUpdate)
|
|
368
653
|
} else {
|
|
369
|
-
|
|
654
|
+
document.write(row, { expose: 'all', store: false })
|
|
655
|
+
document.setPersistedState(row)
|
|
656
|
+
}
|
|
370
657
|
|
|
371
|
-
|
|
372
|
-
this.
|
|
658
|
+
if (!this.subscribesByDocument.get(documentName)?.size) {
|
|
659
|
+
this.checkDocumentNeedRemove(documentName)
|
|
373
660
|
}
|
|
374
661
|
|
|
375
662
|
return document
|
|
376
663
|
}
|
|
377
664
|
|
|
665
|
+
// Точечная доставка документа ТОЛЬКО инициирующей подписке (init/upgrade/resubscribe).
|
|
666
|
+
// Раньше это шло через ensureDocument -> pushDocumentAction('upsert') с полным сканом
|
|
667
|
+
// всех подписок сущности — на каждый ряд из fetchRows. Теперь — один send на подписку.
|
|
668
|
+
private sendDocumentToSubscribe(subscribe: CuboCrdtServerSubscribe, document: CuboCrdtServerDocument, entity_id: number) {
|
|
669
|
+
this.linkSubscribeToDocument(subscribe.id, document.name)
|
|
670
|
+
|
|
671
|
+
this.sendToClient(subscribe.client_id, {
|
|
672
|
+
action: 'create',
|
|
673
|
+
entity: subscribe.entity,
|
|
674
|
+
entity_id,
|
|
675
|
+
data: Array.from(document.stateAsUpdate),
|
|
676
|
+
subscribe_id: subscribe.id
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
if (subscribe.awareness && document.awareness) {
|
|
680
|
+
const ids = Array.from(document.awareness.getStates().keys())
|
|
681
|
+
if (ids.length) {
|
|
682
|
+
this.sendToClient(subscribe.client_id, {
|
|
683
|
+
action: 'awareness',
|
|
684
|
+
entity: subscribe.entity,
|
|
685
|
+
entity_id,
|
|
686
|
+
data: Array.from(encodeAwarenessUpdate(document.awareness, ids)),
|
|
687
|
+
subscribe_id: subscribe.id
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
378
693
|
public deleteDocument(entity: E, row: any, origin?: CuboCrdtClientDocOrigin) {
|
|
379
694
|
if (this.debug) {
|
|
380
695
|
console.log('[CRDT] delete document', entity, row.id, origin)
|
|
@@ -387,6 +702,15 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
387
702
|
// сначала пушим удаление документа
|
|
388
703
|
this.pushDocumentAction('delete', entity, document, null, origin)
|
|
389
704
|
|
|
705
|
+
// чистим обратный индекс: убираем документ из documentsBySubscribe у всех его подписок
|
|
706
|
+
const subs = this.subscribesByDocument.get(documentName)
|
|
707
|
+
if (subs) {
|
|
708
|
+
for (const subscribe_id of subs) {
|
|
709
|
+
this.documentsBySubscribe.get(subscribe_id)?.delete(documentName)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
this.subscribesByDocument.delete(documentName)
|
|
713
|
+
|
|
390
714
|
// удаляем документ
|
|
391
715
|
this.documents.delete(documentName)
|
|
392
716
|
|
|
@@ -402,11 +726,19 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
402
726
|
private onDocumentExternalUpdate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
|
|
403
727
|
let document = this.getDocument(action.entity, action.entity_id)
|
|
404
728
|
if (!document) {
|
|
729
|
+
// документ мог быть вытеснен (эвикция при отключении всех подписчиков), пока
|
|
730
|
+
// клиент ещё шлёт апдейты. Пересоздаём под ПРАВИЛЬНЫМ ключом ({ id }, а не число),
|
|
731
|
+
// без рассылки пустого 'create'. Раньше сюда передавался entity_id как row ->
|
|
732
|
+
// row.id === undefined -> общий документ-призрак `entity:undefined`.
|
|
733
|
+
if (action.entity_id == null) {
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
|
|
405
737
|
if (this.debug) {
|
|
406
738
|
console.warn('[CRDT] onDocumentExternalUpdate - document not found, creating:', action.entity, action.entity_id)
|
|
407
739
|
}
|
|
408
740
|
|
|
409
|
-
document = this.
|
|
741
|
+
document = this.getOrCreateDocument(action.entity as E, { id: action.entity_id }, true)
|
|
410
742
|
}
|
|
411
743
|
|
|
412
744
|
document.applyUpdate(new Uint8Array(update), action.origin)
|
|
@@ -468,12 +800,25 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
468
800
|
const sutableSubscribes = Array.from(this.subscribesByEntity.get(entity) || [])
|
|
469
801
|
.map((subscribe_id) => {
|
|
470
802
|
const subscribe = this.subscribes.get(subscribe_id)
|
|
803
|
+
if (!subscribe) {
|
|
804
|
+
return null
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// аварнесс-события (курсоры/выделения) раскатываем ТОЛЬКО подпискам с awareness,
|
|
808
|
+
// а не всем спискам сущности, которым awareness не нужен
|
|
809
|
+
if (action === 'awareness' && !subscribe.awareness) {
|
|
810
|
+
return null
|
|
811
|
+
}
|
|
471
812
|
|
|
472
813
|
const subscribeSutable = this.checkSubscribeIsSutable(origin, subscribe)
|
|
473
814
|
const documentExistsInSubscribe = this.subscribesByDocument.get(document.name)?.has(subscribe.id)
|
|
474
|
-
const rowSutable =
|
|
815
|
+
const rowSutable = subscribe.paginated
|
|
816
|
+
? documentExistsInSubscribe
|
|
817
|
+
: this.checkRowIsSutable(row, subscribe, entity)
|
|
475
818
|
|
|
476
|
-
const sutable = subscribeSutable && (
|
|
819
|
+
const sutable = subscribeSutable && (subscribe.paginated
|
|
820
|
+
? documentExistsInSubscribe
|
|
821
|
+
: rowSutable || documentExistsInSubscribe)
|
|
477
822
|
|
|
478
823
|
if (sutable) {
|
|
479
824
|
if (this.debug && !rowSutable) {
|
|
@@ -534,8 +879,6 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
534
879
|
.filter((i) => !!i) as { subscribe: CuboCrdtServerSubscribe; action: CuboCrdtAction; data: undefined | number[] }[]
|
|
535
880
|
|
|
536
881
|
if (sutableSubscribes.length) {
|
|
537
|
-
const documentSubscribes = this.subscribesByDocument.get(document.name)
|
|
538
|
-
|
|
539
882
|
sutableSubscribes.forEach(({ subscribe, action, data }) => {
|
|
540
883
|
this.sendToClient(subscribe.client_id, {
|
|
541
884
|
action,
|
|
@@ -549,10 +892,10 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
549
892
|
case 'create':
|
|
550
893
|
case 'upsert':
|
|
551
894
|
case 'update':
|
|
552
|
-
|
|
895
|
+
this.linkSubscribeToDocument(subscribe.id, document.name)
|
|
553
896
|
break
|
|
554
897
|
case 'delete':
|
|
555
|
-
|
|
898
|
+
this.unlinkSubscribeFromDocument(subscribe.id, document.name)
|
|
556
899
|
this.checkDocumentNeedRemove(document.name)
|
|
557
900
|
break
|
|
558
901
|
}
|
|
@@ -576,87 +919,123 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
576
919
|
}
|
|
577
920
|
}
|
|
578
921
|
|
|
579
|
-
private async
|
|
580
|
-
// получаем список всех документов
|
|
581
|
-
const rows = await this.options.fetchRows?.(client, subscribe)
|
|
582
|
-
|
|
583
|
-
// пушим их в сокеты
|
|
584
|
-
for (const row of rows || []) {
|
|
585
|
-
// this.ensureDocument(subscribe.entity as E, row, { subscribes_ids: [subscribe.id] })
|
|
586
|
-
this.ensureDocument(subscribe.entity as E, row, subscribe.awareness)
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// апгрейд подписки (фильтры поменялись например)
|
|
591
|
-
private async upgradeSubscribe(
|
|
922
|
+
private async reconcileSubscribe(
|
|
592
923
|
client: WsServerSocket,
|
|
593
924
|
subscribe: CuboCrdtServerSubscribe,
|
|
594
|
-
|
|
925
|
+
generation: number,
|
|
926
|
+
forceSync: boolean
|
|
595
927
|
) {
|
|
596
|
-
|
|
597
|
-
|
|
928
|
+
const fetched = await this.options.fetchRows?.(client, subscribe)
|
|
929
|
+
const result = Array.isArray(fetched)
|
|
930
|
+
? { rows: fetched, totals: subscribe.totals }
|
|
931
|
+
: { rows: fetched?.rows || [], totals: fetched?.totals || subscribe.totals }
|
|
932
|
+
|
|
933
|
+
if (
|
|
934
|
+
this.subscribes.get(subscribe.id) !== subscribe ||
|
|
935
|
+
this.subscribeGenerations.get(subscribe.id) !== generation
|
|
936
|
+
) {
|
|
937
|
+
return false
|
|
598
938
|
}
|
|
599
939
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
new Promise<void>(async (resolve, reject) => {
|
|
603
|
-
try {
|
|
604
|
-
if (this.debug) {
|
|
605
|
-
console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
|
|
606
|
-
}
|
|
940
|
+
const rowsById = new Map<number, any>()
|
|
941
|
+
const nextIds: number[] = []
|
|
607
942
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
943
|
+
for (const row of result.rows) {
|
|
944
|
+
const id = Number((row as any)?.id)
|
|
945
|
+
if (!Number.isFinite(id) || rowsById.has(id)) {
|
|
946
|
+
continue
|
|
947
|
+
}
|
|
611
948
|
|
|
612
|
-
|
|
949
|
+
rowsById.set(id, row)
|
|
950
|
+
nextIds.push(id)
|
|
951
|
+
}
|
|
613
952
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (this.debug) {
|
|
617
|
-
console.error('[CRDT] upgrade subscribe', e)
|
|
618
|
-
}
|
|
953
|
+
const previousIds = new Set(subscribe.row_ids)
|
|
954
|
+
const nextIdsSet = new Set(nextIds)
|
|
619
955
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
956
|
+
for (const entity_id of subscribe.row_ids) {
|
|
957
|
+
if (nextIdsSet.has(entity_id)) {
|
|
958
|
+
continue
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const documentName = `${subscribe.entity}:${entity_id}`
|
|
962
|
+
this.unlinkSubscribeFromDocument(subscribe.id, documentName)
|
|
963
|
+
this.checkDocumentNeedRemove(documentName)
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
for (const entity_id of nextIds) {
|
|
967
|
+
const row = rowsById.get(entity_id)
|
|
968
|
+
if (!row) {
|
|
969
|
+
continue
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (previousIds.has(entity_id)) {
|
|
973
|
+
this.linkSubscribeToDocument(subscribe.id, `${subscribe.entity}:${entity_id}`)
|
|
974
|
+
continue
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const document = this.getOrCreateDocument(subscribe.entity as E, row, subscribe.awareness)
|
|
978
|
+
this.sendDocumentToSubscribe(subscribe, document, entity_id)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const idsChanged = !areCrdtListIdsEqual(subscribe.row_ids, nextIds)
|
|
982
|
+
const totalsChanged = !areCrdtListTotalsEqual(
|
|
983
|
+
subscribe.totals,
|
|
984
|
+
result.totals
|
|
625
985
|
)
|
|
626
986
|
|
|
627
|
-
|
|
628
|
-
|
|
987
|
+
subscribe.row_ids = nextIds
|
|
988
|
+
subscribe.totals = result.totals
|
|
629
989
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (this.subscribesIniting.has(subscribe.id)) {
|
|
633
|
-
return this.subscribesIniting.get(subscribe.id)
|
|
990
|
+
if (!forceSync && !idsChanged && !totalsChanged) {
|
|
991
|
+
return true
|
|
634
992
|
}
|
|
635
993
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
994
|
+
subscribe.revision += 1
|
|
995
|
+
const data: CuboCrdtListSyncData = {
|
|
996
|
+
ids: nextIds,
|
|
997
|
+
totals: subscribe.totals,
|
|
998
|
+
revision: subscribe.revision
|
|
999
|
+
}
|
|
643
1000
|
|
|
644
|
-
|
|
1001
|
+
this.sendToClient(subscribe.client_id, {
|
|
1002
|
+
action: 'sync',
|
|
1003
|
+
entity: subscribe.entity,
|
|
1004
|
+
subscribe_id: subscribe.id,
|
|
1005
|
+
data
|
|
1006
|
+
})
|
|
645
1007
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (this.debug) {
|
|
649
|
-
console.error('[CRDT] init subscribe', e)
|
|
650
|
-
}
|
|
1008
|
+
return true
|
|
1009
|
+
}
|
|
651
1010
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1011
|
+
// апгрейд подписки (фильтры поменялись например)
|
|
1012
|
+
private async upgradeSubscribe(
|
|
1013
|
+
client: WsServerSocket,
|
|
1014
|
+
subscribe: CuboCrdtServerSubscribe,
|
|
1015
|
+
upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters' | 'paginated'>>
|
|
1016
|
+
) {
|
|
1017
|
+
if (this.debug) {
|
|
1018
|
+
console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (upgrade.filters !== undefined) {
|
|
1022
|
+
subscribe.filters = cloneDeep(upgrade.filters || {})
|
|
1023
|
+
}
|
|
1024
|
+
subscribe.paginated = this.resolvePaginated(
|
|
1025
|
+
subscribe.filters,
|
|
1026
|
+
upgrade.paginated
|
|
657
1027
|
)
|
|
658
1028
|
|
|
659
|
-
return this.
|
|
1029
|
+
return this.requestSubscribeRefresh(subscribe.id, true)
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// инциализация подписки
|
|
1033
|
+
private async initSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
|
|
1034
|
+
if (this.debug) {
|
|
1035
|
+
console.log('[CRDT] init subscribe ' + client.id, subscribe)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return this.requestSubscribeRefresh(subscribe.id, true)
|
|
660
1039
|
}
|
|
661
1040
|
|
|
662
1041
|
private get debug() {
|