@cuboapp/crdt 1.0.14 → 1.0.15
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 +1 -1
- package/src/client/index.ts +6 -1
- package/src/server/document/index.ts +11 -1
- package/src/server/index.ts +194 -91
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -413,6 +413,10 @@ export class CuboCrdtClient<M> {
|
|
|
413
413
|
entity_id: ctx.entity_id,
|
|
414
414
|
data: Array.from(update),
|
|
415
415
|
origin: {
|
|
416
|
+
// по умолчанию 'other' — исходную подписку исключаем (её yjs-документ уже
|
|
417
|
+
// применил изменение локально). Без этого апдейты от TipTap/y-prosemirror
|
|
418
|
+
// приходят без expose и сервер эхом шлёт их обратно самому автору.
|
|
419
|
+
expose: 'other',
|
|
416
420
|
...pick(origin || {}, ['store', 'keys', 'expose']),
|
|
417
421
|
subscribe_id: opts?.subscribe_id
|
|
418
422
|
}
|
|
@@ -468,7 +472,8 @@ export class CuboCrdtClient<M> {
|
|
|
468
472
|
entity: ctx.entity,
|
|
469
473
|
entity_id: ctx.entity_id,
|
|
470
474
|
data: Array.from(update),
|
|
471
|
-
|
|
475
|
+
// 'other' — не шлём свой же курсор обратно исходной подписке
|
|
476
|
+
origin: { expose: 'other', subscribe_id: opts.subscribe_id }
|
|
472
477
|
}
|
|
473
478
|
})
|
|
474
479
|
})
|
|
@@ -94,8 +94,18 @@ export class CuboCrdtServerDocument {
|
|
|
94
94
|
async write(dto: object, origin?: CuboCrdtServerDocumentOrigin) {
|
|
95
95
|
const map = this.getMap()
|
|
96
96
|
|
|
97
|
+
// Пропускаем скалярные ключи с тем же значением: Y.Map.set всегда создаёт новый Item и
|
|
98
|
+
// тумбстонит старый (безостановочный рост документа + лишний update-эвент), даже если
|
|
99
|
+
// значение не изменилось. Сравнение по ссылке — дёшево и не трогает объекты (для них
|
|
100
|
+
// set выполняется как и раньше, без лишней сериализации на горячем пути).
|
|
101
|
+
const entries = Object.entries(dto).filter(([key, value]) => map.get(key) !== value)
|
|
102
|
+
|
|
103
|
+
if (!entries.length) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
this.ydoc.transact(() => {
|
|
98
|
-
|
|
108
|
+
entries.forEach(([key, value]) => {
|
|
99
109
|
map.set(key, value)
|
|
100
110
|
})
|
|
101
111
|
}, origin)
|
package/src/server/index.ts
CHANGED
|
@@ -36,6 +36,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
36
36
|
|
|
37
37
|
private documents = new Map<string, CuboCrdtServerDocument>()
|
|
38
38
|
private subscribesByDocument = new Map<string, Set<string>>()
|
|
39
|
+
// обратный индекс: подписка -> имена документов, к которым она привязана.
|
|
40
|
+
// Нужен, чтобы отписка/очистка не сканировала ВСЕ документы сервера (O(subs x docs)).
|
|
41
|
+
private documentsBySubscribe = new Map<string, Set<string>>()
|
|
39
42
|
|
|
40
43
|
public async init() {
|
|
41
44
|
this.entities.forEach((e) => this.subscribesByEntity.set(e, new Set()))
|
|
@@ -163,8 +166,9 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
public removeClient(client: WsServerSocket) {
|
|
166
|
-
// удаляем все подписки по клиенту
|
|
167
|
-
|
|
169
|
+
// удаляем все подписки по клиенту (но только те, что всё ещё принадлежат ЭТОМУ сокету —
|
|
170
|
+
// подписка могла быть переподвязана к новому сокету при reconnect с тем же subscribe_id)
|
|
171
|
+
this.subscribesByClient.get(client.id)?.forEach((subscribe_id) => this.cleanSubscribe(subscribe_id, client.id))
|
|
168
172
|
|
|
169
173
|
// удаляем мапу подписок по клиенту
|
|
170
174
|
this.subscribesByClient.delete(client.id)
|
|
@@ -198,17 +202,17 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
198
202
|
return
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
//
|
|
202
|
-
|
|
205
|
+
// Дедупликация побайтово идентичных событий внутри батча + сборка кадра за ОДИН проход
|
|
206
|
+
// сериализации: сериализуем каждый элемент один раз (он же служит ключом дедупа) и
|
|
207
|
+
// склеиваем готовые JSON-строки, а не сериализуем весь массив повторно.
|
|
208
|
+
const items = new Set<string>()
|
|
209
|
+
for (const item of queue) {
|
|
210
|
+
items.add(JSON.stringify(item))
|
|
211
|
+
}
|
|
203
212
|
|
|
204
213
|
queue.clear()
|
|
205
214
|
|
|
206
|
-
client.send(
|
|
207
|
-
JSON.stringify({
|
|
208
|
-
method: CUBO_CRDT_EVENT.EVENT,
|
|
209
|
-
data
|
|
210
|
-
})
|
|
211
|
-
)
|
|
215
|
+
client.send(`{"method":${JSON.stringify(CUBO_CRDT_EVENT.EVENT)},"data":[${Array.from(items).join(',')}]}`)
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
public sendToClient(client_id: string, data: any) {
|
|
@@ -241,6 +245,29 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
241
245
|
}
|
|
242
246
|
}
|
|
243
247
|
|
|
248
|
+
// привязываем подписку к документу (в обоих индексах)
|
|
249
|
+
private linkSubscribeToDocument(subscribe_id: string, docName: string) {
|
|
250
|
+
let byDoc = this.subscribesByDocument.get(docName)
|
|
251
|
+
if (!byDoc) {
|
|
252
|
+
byDoc = new Set()
|
|
253
|
+
this.subscribesByDocument.set(docName, byDoc)
|
|
254
|
+
}
|
|
255
|
+
byDoc.add(subscribe_id)
|
|
256
|
+
|
|
257
|
+
let bySub = this.documentsBySubscribe.get(subscribe_id)
|
|
258
|
+
if (!bySub) {
|
|
259
|
+
bySub = new Set()
|
|
260
|
+
this.documentsBySubscribe.set(subscribe_id, bySub)
|
|
261
|
+
}
|
|
262
|
+
bySub.add(docName)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// отвязываем подписку от документа (в обоих индексах)
|
|
266
|
+
private unlinkSubscribeFromDocument(subscribe_id: string, docName: string) {
|
|
267
|
+
this.subscribesByDocument.get(docName)?.delete(subscribe_id)
|
|
268
|
+
this.documentsBySubscribe.get(subscribe_id)?.delete(docName)
|
|
269
|
+
}
|
|
270
|
+
|
|
244
271
|
// если у документа нет подписок - сносим его
|
|
245
272
|
private checkDocumentNeedRemove(name: string) {
|
|
246
273
|
if (!this.subscribesByDocument.get(name)?.size) {
|
|
@@ -250,55 +277,62 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
250
277
|
|
|
251
278
|
this.documents.get(name)?.destroy()
|
|
252
279
|
this.documents.delete(name)
|
|
280
|
+
// не оставляем пустой Set в индексе (иначе — медленная утечка записей мапы);
|
|
281
|
+
// getOrCreateDocument пересоздаст его при повторном появлении документа
|
|
282
|
+
this.subscribesByDocument.delete(name)
|
|
253
283
|
}
|
|
254
284
|
}
|
|
255
285
|
|
|
256
|
-
// очистка подписки (при отписке клиента - через emit-метод или options-хук)
|
|
257
|
-
|
|
286
|
+
// очистка подписки (при отписке клиента - через emit-метод или options-хук).
|
|
287
|
+
// ownerClientId (опционально): при отключении сокета передаётся id этого сокета —
|
|
288
|
+
// если подписка уже переподвязана к НОВОМУ клиенту (reconnect с тем же subscribe_id),
|
|
289
|
+
// старый close-хендлер не должен её сносить.
|
|
290
|
+
private cleanSubscribe(subscribe_id: string, ownerClientId?: string) {
|
|
258
291
|
const subscribe = this.subscribes.get(subscribe_id)
|
|
259
|
-
if (subscribe) {
|
|
292
|
+
if (!subscribe) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// подписка была переподвязана к другому (более новому) клиенту — не трогаем
|
|
297
|
+
if (ownerClientId !== undefined && subscribe.client_id !== ownerClientId) {
|
|
260
298
|
if (this.debug) {
|
|
261
|
-
console.log('[CRDT]
|
|
299
|
+
console.log('[CRDT] cleanSubscribe skipped (rebound to newer client)', subscribe_id)
|
|
262
300
|
}
|
|
301
|
+
return
|
|
302
|
+
}
|
|
263
303
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (!subs.has(subscribe_id)) {
|
|
268
|
-
return
|
|
269
|
-
}
|
|
304
|
+
if (this.debug) {
|
|
305
|
+
console.log('[CRDT] unsubscribe', subscribe.entity, subscribe.filters)
|
|
306
|
+
}
|
|
270
307
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return
|
|
274
|
-
}
|
|
308
|
+
// документы этой подписки берём из обратного индекса, а не сканируем все
|
|
309
|
+
const docNames = this.documentsBySubscribe.get(subscribe_id)
|
|
275
310
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
311
|
+
if (docNames) {
|
|
312
|
+
for (const docName of docNames) {
|
|
313
|
+
if (subscribe.awareness) {
|
|
314
|
+
//убираем аварнесс стейт при отписке
|
|
315
|
+
const doc = this.documents.get(docName)
|
|
316
|
+
const ids = doc?.awareness ? Array.from(doc.awarenessBySubscribe.get(subscribe_id) || []) : []
|
|
317
|
+
if (doc?.awareness && ids.length) {
|
|
318
|
+
removeAwarenessStates(doc.awareness, ids, { subscribe_id, expose: 'all' })
|
|
319
|
+
doc.awarenessBySubscribe.delete(subscribe_id)
|
|
279
320
|
}
|
|
321
|
+
}
|
|
280
322
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
})
|
|
323
|
+
// отписываем документ от подписки (только по одной стороне индекса — другую чистим ниже)
|
|
324
|
+
this.subscribesByDocument.get(docName)?.delete(subscribe_id)
|
|
325
|
+
this.checkDocumentNeedRemove(docName)
|
|
285
326
|
}
|
|
327
|
+
}
|
|
286
328
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
// отписываем все документы от подписки
|
|
291
|
-
this.subscribesByDocument.forEach((subscribes, docName) => {
|
|
292
|
-
subscribes.delete(subscribe_id)
|
|
293
|
-
|
|
294
|
-
this.checkDocumentNeedRemove(docName)
|
|
295
|
-
})
|
|
329
|
+
// удаляем обратный индекс
|
|
330
|
+
this.documentsBySubscribe.delete(subscribe_id)
|
|
296
331
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
332
|
+
// удаляем подписки
|
|
333
|
+
this.subscribes.delete(subscribe_id)
|
|
334
|
+
this.subscribesByClient.get(subscribe.client_id)?.delete(subscribe_id)
|
|
335
|
+
this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe_id)
|
|
302
336
|
}
|
|
303
337
|
|
|
304
338
|
public getDocument(entity: string, entity_id: number) {
|
|
@@ -321,60 +355,97 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
321
355
|
})
|
|
322
356
|
}
|
|
323
357
|
|
|
324
|
-
|
|
325
|
-
|
|
358
|
+
// создаёт (или возвращает существующий) серверный документ БЕЗ рассылки подписчикам.
|
|
359
|
+
// Используется как для рассылочного пути (ensureDocument), так и для точечной
|
|
360
|
+
// доставки на init подписки (pushSubscribeDocuments).
|
|
361
|
+
private getOrCreateDocument(entity: E, row: any, awareness?: boolean): CuboCrdtServerDocument {
|
|
326
362
|
const entity_id = row.id
|
|
327
363
|
const documentName = `${entity}:${entity_id}`
|
|
328
364
|
|
|
329
|
-
// проверяем документ
|
|
330
365
|
let document = this.documents.get(documentName)
|
|
331
|
-
if (
|
|
332
|
-
document
|
|
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)
|
|
345
|
-
|
|
346
|
-
this.pushDocumentAction('awareness', entity, document!, data, origin)
|
|
347
|
-
},
|
|
348
|
-
onStore: (item: object, origin: CuboCrdtServerDocumentOrigin) => {
|
|
349
|
-
const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
|
|
350
|
-
const client = subscribe && this.clients.get(subscribe.client_id)
|
|
366
|
+
if (document) {
|
|
367
|
+
return document
|
|
368
|
+
}
|
|
351
369
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
370
|
+
document = new CuboCrdtServerDocument({
|
|
371
|
+
name: documentName,
|
|
372
|
+
initialState: row,
|
|
373
|
+
awareness: awareness,
|
|
374
|
+
onUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
375
|
+
this.pushDocumentAction('update', entity, document!, data, origin)
|
|
376
|
+
},
|
|
377
|
+
onAwarenessUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
378
|
+
this.pushDocumentAction('awareness', entity, document!, data, origin)
|
|
379
|
+
},
|
|
380
|
+
onStore: (item: object, origin: CuboCrdtServerDocumentOrigin) => {
|
|
381
|
+
const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
|
|
382
|
+
const client = subscribe && this.clients.get(subscribe.client_id)
|
|
383
|
+
|
|
384
|
+
if (client) {
|
|
385
|
+
return this.options?.storeRow?.(entity as any, entity_id, item as any, { document, client, origin })
|
|
355
386
|
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// создаём документ
|
|
359
|
-
this.documents.set(documentName, document)
|
|
387
|
+
}
|
|
388
|
+
})
|
|
360
389
|
|
|
361
|
-
|
|
390
|
+
this.documents.set(documentName, document)
|
|
391
|
+
if (!this.subscribesByDocument.has(documentName)) {
|
|
362
392
|
this.subscribesByDocument.set(documentName, new Set())
|
|
393
|
+
}
|
|
363
394
|
|
|
364
|
-
|
|
365
|
-
|
|
395
|
+
return document
|
|
396
|
+
}
|
|
366
397
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
// Рассылочный путь (хук onAfterCreate/onAfterUpdate из бэка): создаёт документ и
|
|
399
|
+
// раскатывает 'create'/'upsert' по ВСЕМ подходящим подпискам сущности.
|
|
400
|
+
public ensureDocument(entity: E, row: any, awareness?: boolean) {
|
|
401
|
+
if (row?.id == null) {
|
|
402
|
+
if (this.debug) {
|
|
403
|
+
console.warn('[CRDT] ensureDocument: row without id', entity, row)
|
|
404
|
+
}
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const documentName = `${entity}:${row.id}`
|
|
409
|
+
const existed = this.documents.has(documentName)
|
|
410
|
+
const document = this.getOrCreateDocument(entity, row, awareness)
|
|
370
411
|
|
|
371
|
-
|
|
412
|
+
if (!existed) {
|
|
413
|
+
this.pushDocumentAction('create', entity, document, document.stateAsUpdate)
|
|
414
|
+
} else {
|
|
372
415
|
this.pushDocumentAction('upsert', entity, document, document.stateAsUpdate)
|
|
373
416
|
}
|
|
374
417
|
|
|
375
418
|
return document
|
|
376
419
|
}
|
|
377
420
|
|
|
421
|
+
// Точечная доставка документа ТОЛЬКО инициирующей подписке (init/upgrade/resubscribe).
|
|
422
|
+
// Раньше это шло через ensureDocument -> pushDocumentAction('upsert') с полным сканом
|
|
423
|
+
// всех подписок сущности — на каждый ряд из fetchRows. Теперь — один send на подписку.
|
|
424
|
+
private sendDocumentToSubscribe(subscribe: CuboCrdtServerSubscribe, document: CuboCrdtServerDocument, entity_id: number) {
|
|
425
|
+
this.linkSubscribeToDocument(subscribe.id, document.name)
|
|
426
|
+
|
|
427
|
+
this.sendToClient(subscribe.client_id, {
|
|
428
|
+
action: 'create',
|
|
429
|
+
entity: subscribe.entity,
|
|
430
|
+
entity_id,
|
|
431
|
+
data: Array.from(document.stateAsUpdate),
|
|
432
|
+
subscribe_id: subscribe.id
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
if (subscribe.awareness && document.awareness) {
|
|
436
|
+
const ids = Array.from(document.awareness.getStates().keys())
|
|
437
|
+
if (ids.length) {
|
|
438
|
+
this.sendToClient(subscribe.client_id, {
|
|
439
|
+
action: 'awareness',
|
|
440
|
+
entity: subscribe.entity,
|
|
441
|
+
entity_id,
|
|
442
|
+
data: Array.from(encodeAwarenessUpdate(document.awareness, ids)),
|
|
443
|
+
subscribe_id: subscribe.id
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
378
449
|
public deleteDocument(entity: E, row: any, origin?: CuboCrdtClientDocOrigin) {
|
|
379
450
|
if (this.debug) {
|
|
380
451
|
console.log('[CRDT] delete document', entity, row.id, origin)
|
|
@@ -387,6 +458,15 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
387
458
|
// сначала пушим удаление документа
|
|
388
459
|
this.pushDocumentAction('delete', entity, document, null, origin)
|
|
389
460
|
|
|
461
|
+
// чистим обратный индекс: убираем документ из documentsBySubscribe у всех его подписок
|
|
462
|
+
const subs = this.subscribesByDocument.get(documentName)
|
|
463
|
+
if (subs) {
|
|
464
|
+
for (const subscribe_id of subs) {
|
|
465
|
+
this.documentsBySubscribe.get(subscribe_id)?.delete(documentName)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
this.subscribesByDocument.delete(documentName)
|
|
469
|
+
|
|
390
470
|
// удаляем документ
|
|
391
471
|
this.documents.delete(documentName)
|
|
392
472
|
|
|
@@ -402,11 +482,19 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
402
482
|
private onDocumentExternalUpdate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
|
|
403
483
|
let document = this.getDocument(action.entity, action.entity_id)
|
|
404
484
|
if (!document) {
|
|
485
|
+
// документ мог быть вытеснен (эвикция при отключении всех подписчиков), пока
|
|
486
|
+
// клиент ещё шлёт апдейты. Пересоздаём под ПРАВИЛЬНЫМ ключом ({ id }, а не число),
|
|
487
|
+
// без рассылки пустого 'create'. Раньше сюда передавался entity_id как row ->
|
|
488
|
+
// row.id === undefined -> общий документ-призрак `entity:undefined`.
|
|
489
|
+
if (action.entity_id == null) {
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
|
|
405
493
|
if (this.debug) {
|
|
406
494
|
console.warn('[CRDT] onDocumentExternalUpdate - document not found, creating:', action.entity, action.entity_id)
|
|
407
495
|
}
|
|
408
496
|
|
|
409
|
-
document = this.
|
|
497
|
+
document = this.getOrCreateDocument(action.entity as E, { id: action.entity_id }, true)
|
|
410
498
|
}
|
|
411
499
|
|
|
412
500
|
document.applyUpdate(new Uint8Array(update), action.origin)
|
|
@@ -468,6 +556,15 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
468
556
|
const sutableSubscribes = Array.from(this.subscribesByEntity.get(entity) || [])
|
|
469
557
|
.map((subscribe_id) => {
|
|
470
558
|
const subscribe = this.subscribes.get(subscribe_id)
|
|
559
|
+
if (!subscribe) {
|
|
560
|
+
return null
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// аварнесс-события (курсоры/выделения) раскатываем ТОЛЬКО подпискам с awareness,
|
|
564
|
+
// а не всем спискам сущности, которым awareness не нужен
|
|
565
|
+
if (action === 'awareness' && !subscribe.awareness) {
|
|
566
|
+
return null
|
|
567
|
+
}
|
|
471
568
|
|
|
472
569
|
const subscribeSutable = this.checkSubscribeIsSutable(origin, subscribe)
|
|
473
570
|
const documentExistsInSubscribe = this.subscribesByDocument.get(document.name)?.has(subscribe.id)
|
|
@@ -534,8 +631,6 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
534
631
|
.filter((i) => !!i) as { subscribe: CuboCrdtServerSubscribe; action: CuboCrdtAction; data: undefined | number[] }[]
|
|
535
632
|
|
|
536
633
|
if (sutableSubscribes.length) {
|
|
537
|
-
const documentSubscribes = this.subscribesByDocument.get(document.name)
|
|
538
|
-
|
|
539
634
|
sutableSubscribes.forEach(({ subscribe, action, data }) => {
|
|
540
635
|
this.sendToClient(subscribe.client_id, {
|
|
541
636
|
action,
|
|
@@ -549,10 +644,10 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
549
644
|
case 'create':
|
|
550
645
|
case 'upsert':
|
|
551
646
|
case 'update':
|
|
552
|
-
|
|
647
|
+
this.linkSubscribeToDocument(subscribe.id, document.name)
|
|
553
648
|
break
|
|
554
649
|
case 'delete':
|
|
555
|
-
|
|
650
|
+
this.unlinkSubscribeFromDocument(subscribe.id, document.name)
|
|
556
651
|
this.checkDocumentNeedRemove(document.name)
|
|
557
652
|
break
|
|
558
653
|
}
|
|
@@ -577,13 +672,21 @@ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extr
|
|
|
577
672
|
}
|
|
578
673
|
|
|
579
674
|
private async pushSubscribeDocuments(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
|
|
580
|
-
// получаем
|
|
675
|
+
// получаем строки под фильтры ЭТОЙ подписки
|
|
581
676
|
const rows = await this.options.fetchRows?.(client, subscribe)
|
|
582
677
|
|
|
583
|
-
//
|
|
678
|
+
// Доставляем документы ТОЧЕЧНО инициирующей подписке. Раньше это шло через
|
|
679
|
+
// ensureDocument -> pushDocumentAction('upsert') с полным сканом всех подписок сущности
|
|
680
|
+
// НА КАЖДУЮ строку из fetchRows (до 10k) — главный усилитель нагрузки при (ре)подписке.
|
|
681
|
+
// Строки уже отфильтрованы под фильтры подписки в fetchRows, поэтому повторная
|
|
682
|
+
// проверка/рассылка остальным не нужна: живые create/update придут через onAfterCreate.
|
|
584
683
|
for (const row of rows || []) {
|
|
585
|
-
|
|
586
|
-
|
|
684
|
+
if ((row as any)?.id == null) {
|
|
685
|
+
continue
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const document = this.getOrCreateDocument(subscribe.entity as E, row, subscribe.awareness)
|
|
689
|
+
this.sendDocumentToSubscribe(subscribe, document, (row as any).id)
|
|
587
690
|
}
|
|
588
691
|
}
|
|
589
692
|
|