@cuboapp/crdt 1.0.6 → 1.0.8

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.
@@ -1,9 +1,11 @@
1
+ import { cloneDeep } from '@cuboapp/utils'
1
2
  import { WsServerSocket } from '@cuboapp/ws'
2
3
 
3
4
  import { CUBO_CRDT_EVENT } from '../constants'
4
5
  import { CuboCrdtAction } from '../types'
5
6
  import { checkRowIsSutable } from '../utils'
6
7
 
8
+ import { CuboCrdtClientDocOrigin } from '../client'
7
9
  import { CuboCrdtServerDocument } from './document'
8
10
  import {
9
11
  CuboCrdtServerDocumentIncomingAction,
@@ -11,27 +13,31 @@ import {
11
13
  CuboCrdtServerOptions,
12
14
  CuboCrdtServerSubscribe,
13
15
  CuboCrdtServerSubscribeDto,
14
- CuboCrdtServerUnsubscribeDto
16
+ CuboCrdtServerUnsubscribeDto,
17
+ CuboCrdtSocketClient
15
18
  } from './types'
16
19
 
17
20
  export * from './document'
18
21
  export * from './types'
19
22
 
20
- export class CuboCrdtServer<M, A = {}> {
21
- constructor(private options: CuboCrdtServerOptions<M, A>) {}
23
+ export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extract<keyof M, string>> {
24
+ constructor(private options: CuboCrdtServerOptions<M, A, E>) {}
22
25
 
23
- private clients = new Map<string, WsServerSocket<{ auth?: A }>>()
26
+ private clients = new Map<string, CuboCrdtSocketClient<A>>()
27
+ private queue = new Map<string, Set<any>>()
28
+ private sendTimeouts = new Map<string, any>()
24
29
 
25
30
  private subscribes = new Map<string, CuboCrdtServerSubscribe>()
26
- private subscribesByClient = new Map<string, Map<string, CuboCrdtServerSubscribe>>()
27
- private subscribesByEntity = new Map<string, Map<string, CuboCrdtServerSubscribe>>()
31
+ private subscribesByClient = new Map<string, Set<string>>()
32
+ private subscribesByEntity = new Map<E, Set<string>>()
28
33
  private subscribesIniting = new Map<string, Promise<void>>()
34
+ private subscribesUpgrading = new Map<string, Promise<void>>()
29
35
 
30
36
  private documents = new Map<string, CuboCrdtServerDocument>()
31
- private documentsSubsrcibes = new Map<string, Set<string>>()
37
+ private subscribesByDocument = new Map<string, Set<string>>()
32
38
 
33
39
  public async init() {
34
- this.entities.forEach((e) => this.subscribesByEntity.set(e, new Map()))
40
+ this.entities.forEach((e) => this.subscribesByEntity.set(e, new Set()))
35
41
 
36
42
  this.ws.registerHandler(CUBO_CRDT_EVENT.SUBSCRIBE, async ({ client, message }) => {
37
43
  const { subscribe_id: id, entity, filters } = message.data as CuboCrdtServerSubscribeDto
@@ -50,14 +56,31 @@ export class CuboCrdtServer<M, A = {}> {
50
56
  this.subscribes.set(id, subscribe)
51
57
 
52
58
  // создаём мапу подписок по клиенту
53
- this.subscribesByClient.get(client.id)?.set(id, subscribe)
59
+ this.subscribesByClient.get(client.id)?.add(id)
54
60
 
55
61
  // создаём мапу подписок по сущности
56
- this.subscribesByEntity.get(entity)?.set(id, subscribe)
62
+ this.subscribesByEntity.get(entity as E)?.add(id)
57
63
 
64
+ // инициализируем подписку
58
65
  this.initSubscribe(client, subscribe)
59
66
  })
60
67
 
68
+ this.ws.registerHandler(CUBO_CRDT_EVENT.UPGRADE, async ({ client, message }) => {
69
+ const { subscribe_id: id, entity, filters } = message.data as CuboCrdtServerSubscribeDto
70
+
71
+ if (this.debug) {
72
+ console.log('[CRDT] upgrade', entity, filters)
73
+ }
74
+
75
+ const subscribe = this.subscribes.get(id)
76
+ if (!subscribe) {
77
+ return
78
+ }
79
+
80
+ // обновляем подписку
81
+ this.upgradeSubscribe(client, subscribe, { filters })
82
+ })
83
+
61
84
  this.ws.registerHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE, async ({ message }) => {
62
85
  const { subscribe_id } = message.data as CuboCrdtServerUnsubscribeDto
63
86
 
@@ -108,7 +131,7 @@ export class CuboCrdtServer<M, A = {}> {
108
131
  size: this.documents.size,
109
132
  rows: Array.from(this.documents).map((d) => ({
110
133
  id: d[0],
111
- subs: Array.from(this.documentsSubsrcibes.get(d[0]) || [])
134
+ subs: Array.from(this.subscribesByDocument.get(d[0]) || [])
112
135
  }))
113
136
  }
114
137
  }
@@ -128,20 +151,82 @@ export class CuboCrdtServer<M, A = {}> {
128
151
  this.clients.set(client.id, client)
129
152
 
130
153
  // создаем мапу подписок по клиенту
131
- this.subscribesByClient.set(client.id, new Map())
154
+ this.subscribesByClient.set(client.id, new Set())
155
+
156
+ // создаём очередь под клиента
157
+ this.queue.set(client.id, new Set())
132
158
  }
133
159
 
134
160
  public removeClient(client: WsServerSocket) {
135
161
  // удаляем все подписки по клиенту
136
- this.subscribesByClient.get(client.id)?.forEach((sub) => this.cleanSubscribe(sub.id))
162
+ this.subscribesByClient.get(client.id)?.forEach((subscribe_id) => this.cleanSubscribe(subscribe_id))
137
163
 
138
164
  // удаляем мапу подписок по клиенту
139
165
  this.subscribesByClient.delete(client.id)
140
166
 
167
+ // удаляем очередь под клиента
168
+ this.queue.delete(client.id)
169
+
141
170
  // и в принципе клиента
142
171
  this.clients.delete(client.id)
143
172
  }
144
173
 
174
+ private sendQueueToClient(client: CuboCrdtSocketClient<A>) {
175
+ const queue = this.queue.get(client.id)
176
+
177
+ // const data = Array.from(queue)
178
+ const data = [...new Map((Array.from(queue) || []).map((item) => [JSON.stringify(item), item])).values()]
179
+
180
+ queue.clear()
181
+
182
+ client.send(
183
+ JSON.stringify({
184
+ method: CUBO_CRDT_EVENT.EVENT,
185
+ data
186
+ })
187
+ )
188
+
189
+ queue.clear()
190
+ }
191
+
192
+ public sendToClient(client_id: string, data: any) {
193
+ const client = this.clients.get(client_id)
194
+ if (!client) {
195
+ return
196
+ }
197
+
198
+ // сбрасываем таймаут если он есть
199
+ clearTimeout(this.sendTimeouts.get(client_id))
200
+
201
+ // добавляем в очередь
202
+ this.queue.get(client_id).add(data)
203
+
204
+ // если очередь >10 то сразу отправляем
205
+ if (this.queue.size > 10) {
206
+ this.sendQueueToClient(client)
207
+ }
208
+ // если нет - ставим таймаут
209
+ else {
210
+ this.sendTimeouts.set(
211
+ client_id,
212
+ setTimeout(() => this.sendQueueToClient(client), 100)
213
+ )
214
+ }
215
+ }
216
+
217
+ // если у документа нет подписок - сносим его
218
+ private checkDocumentNeedRemove(name: string) {
219
+ if (!this.subscribesByDocument.get(name)?.size) {
220
+ if (this.debug) {
221
+ console.log('[CRDT] delete document', name)
222
+ }
223
+
224
+ this.documents.get(name)?.destroy()
225
+ this.documents.delete(name)
226
+ }
227
+ }
228
+
229
+ // очистка подписки (при отписке клиента - через emit-метод или options-хук)
145
230
  private cleanSubscribe(subscribe_id: string) {
146
231
  const subscribe = this.subscribes.get(subscribe_id)
147
232
  if (subscribe) {
@@ -150,26 +235,19 @@ export class CuboCrdtServer<M, A = {}> {
150
235
  }
151
236
 
152
237
  // удаляем подписку по сущности
153
- this.subscribesByEntity.get(subscribe.entity)?.delete(subscribe.id)
238
+ this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe.id)
154
239
 
155
- // отписываем все документы
156
- this.documentsSubsrcibes.forEach((subs, docName) => {
157
- subs.delete(subscribe_id)
158
-
159
- if (!subs?.size) {
160
- if (this.debug) {
161
- console.log('[CRDT] delete document', docName)
162
- }
240
+ // отписываем все документы от подписки
241
+ this.subscribesByDocument.forEach((subscribes, docName) => {
242
+ subscribes.delete(subscribe_id)
163
243
 
164
- this.documents.get(docName)?.destroy()
165
- this.documents.delete(docName)
166
- }
244
+ this.checkDocumentNeedRemove(docName)
167
245
  })
168
246
 
169
247
  // удаляем подписки
170
248
  this.subscribes.delete(subscribe_id)
171
249
  this.subscribesByClient.get(subscribe.client_id)?.delete(subscribe_id)
172
- this.subscribesByEntity.get(subscribe.entity)?.delete(subscribe_id)
250
+ this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe_id)
173
251
  }
174
252
  }
175
253
 
@@ -178,12 +256,22 @@ export class CuboCrdtServer<M, A = {}> {
178
256
  }
179
257
 
180
258
  public getSutableSubscribes(entity: string, row: any) {
181
- return Array.from(this.subscribesByEntity.get(entity) || [])
182
- .filter((m) => checkRowIsSutable(row, m[1].filters))
259
+ return Array.from(this.subscribesByEntity.get(entity as E) || [])
260
+ .filter((m) => {
261
+ const subscribe = this.subscribes.get(m[1])
262
+
263
+ let state = subscribe && checkRowIsSutable(row, subscribe)
264
+ if (this.options.checkRowIsSutable) {
265
+ state = this.options.checkRowIsSutable(state, { entity: entity as any, subscribe, row })
266
+ }
267
+
268
+ return state
269
+ })
183
270
  .map((m) => m[1])
184
271
  }
185
272
 
186
- public ensureDocument(entity: string, row: any, subscribes_ids?: string[]) {
273
+ public ensureDocument(entity: E, row: any) {
274
+ // todo: врапнуть всё это в Promise
187
275
  const entity_id = row.id
188
276
  const documentName = `${entity}:${entity_id}`
189
277
 
@@ -192,11 +280,12 @@ export class CuboCrdtServer<M, A = {}> {
192
280
  if (!document) {
193
281
  document = new CuboCrdtServerDocument({
194
282
  name: documentName,
195
- onCreate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
196
- this.pushDocumentAction('create', document!, data, origin)
197
- },
283
+ initialState: row,
198
284
  onUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
199
- this.pushDocumentAction('update', document!, data, origin)
285
+ // пушим обновление документа
286
+ // console.log('push document update', document.name)
287
+
288
+ this.pushDocumentAction('update', entity, document!, data, origin)
200
289
  },
201
290
  onStore: (item: object, origin: CuboCrdtServerDocumentOrigin) => {
202
291
  const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
@@ -208,47 +297,37 @@ export class CuboCrdtServer<M, A = {}> {
208
297
  }
209
298
  })
210
299
 
300
+ // создаём документ
211
301
  this.documents.set(documentName, document)
212
302
 
213
- this.documentsSubsrcibes.set(documentName, new Set())
214
- if (subscribes_ids?.length) {
215
- const subs = this.documentsSubsrcibes.get(documentName)
216
- subscribes_ids?.forEach((id) => {
217
- // добавляем подписку к документу
218
- subs?.add(id)
219
- })
220
- }
303
+ // создаём пул подписок по документу
304
+ this.subscribesByDocument.set(documentName, new Set())
221
305
 
222
- // инициализируем документ
223
- document.init(row)
306
+ // пушим создание документа
307
+ this.pushDocumentAction('create', entity, document, document.stateAsUpdate)
308
+
309
+ // console.log('create document', documentName)
224
310
  } else {
225
- // добавляем подписки к документу
226
- const subs = this.documentsSubsrcibes.get(documentName)
227
- subscribes_ids?.forEach((subscribe_id) => {
228
- subs?.add(subscribe_id)
311
+ // console.log('update document', documentName)
229
312
 
230
- // пушим документ в сокеты (в текущем состоянии)
231
- this.pushDocumentAction('update', document!, document!.stateAsUpdate, { subscribe_id, expose: 'subscribe' })
232
- })
313
+ // пушим обновление документа
314
+ this.pushDocumentAction('upsert', entity, document, document.stateAsUpdate)
233
315
  }
234
316
 
235
- return this.documents.get(documentName)
317
+ return document
236
318
  }
237
319
 
238
- public deleteDocument(entity: string, row: any) {
320
+ public deleteDocument(entity: E, row: any, origin?: CuboCrdtClientDocOrigin) {
239
321
  if (this.debug) {
240
- console.log('[CRDT] delete document', entity, row.id)
322
+ console.log('[CRDT] delete document', entity, row.id, origin)
241
323
  }
242
324
 
243
325
  const documentName = `${entity}:${row.id}`
244
326
  const document = this.documents.get(documentName)
245
327
 
246
328
  if (document) {
247
- // отписываемся от всех подписок на документ
248
- this.pushDocumentAction('delete', document, null, { expose: 'subscribe' })
249
-
250
- // удаляем все подписки по документу
251
- this.documentsSubsrcibes.delete(documentName)
329
+ // сначала пушим удаление документа
330
+ this.pushDocumentAction('delete', entity, document, null, origin)
252
331
 
253
332
  // удаляем документ
254
333
  this.documents.delete(documentName)
@@ -278,127 +357,186 @@ export class CuboCrdtServer<M, A = {}> {
278
357
  console.warn('[CRDT] onDocumentExternalDelete not implemented')
279
358
  }
280
359
 
281
- private documentUnsubscribe(docName: string, subscribe_id: string, push = true) {
282
- const document = this.documents.get(docName)
360
+ private checkSubscribeIsSutable(origin: CuboCrdtServerDocumentOrigin, subscribe?: CuboCrdtServerSubscribe) {
361
+ if (!subscribe) {
362
+ return false
363
+ }
283
364
 
284
- if (document) {
285
- // сначала отсылаем, затем удаляем подписки (иначе не будет куда слать)
286
- if (push) {
287
- this.pushDocumentAction('delete', document, null, { subscribe_id, expose: 'subscribe' })
288
- }
365
+ const noStrategyResolved = !origin?.expose || origin?.expose === 'all'
366
+ const strategyOtherResolved = (origin?.expose === 'other' && origin.subscribe_id !== subscribe?.id) || false
367
+ const strategyClientResolved = origin?.expose === 'client' && subscribe?.client_id === origin.client_id
368
+ const strategySubscribeResolved = origin?.expose === 'subscribe' && subscribe?.id === origin.subscribe_id
289
369
 
290
- const documentSubscribes = this.documentsSubsrcibes.get(docName)
291
- documentSubscribes?.delete(subscribe_id)
370
+ return noStrategyResolved || strategyOtherResolved || strategyClientResolved || strategySubscribeResolved
371
+ }
292
372
 
293
- // если нет больше подписок - удаляем документ
294
- if (!documentSubscribes?.size) {
295
- if (this.debug) {
296
- console.log('[CRDT] delete document', name)
297
- }
373
+ private checkRowIsSutable(row: any, subscribe: CuboCrdtServerSubscribe, entity: E) {
374
+ const baseState = checkRowIsSutable(row, subscribe.filters || {})
298
375
 
299
- document?.destroy()
300
- this.documents.delete(document.name)
301
- }
376
+ if (!this.options.checkRowIsSutable) {
377
+ return baseState
302
378
  }
379
+
380
+ return this.options.checkRowIsSutable(baseState, { entity, subscribe, row: row as any })
303
381
  }
304
382
 
305
383
  private pushDocumentAction(
306
384
  action: CuboCrdtAction,
385
+ entity: E,
307
386
  document: CuboCrdtServerDocument,
308
387
  data?: Uint8Array | null,
309
388
  origin?: CuboCrdtServerDocumentOrigin
310
389
  ) {
311
390
  const row = document.getJson()
312
391
 
313
- if (this.debug) {
314
- console.log('[CRDT] pushDocumentAction ' + action, {
315
- action,
316
- row_id: row.id,
317
- subscribes: Array.from(this.documentsSubsrcibes.get(document.name)!)
318
- })
319
- }
392
+ // берём все подписки по сущности
393
+ const sutableSubscribes = Array.from(this.subscribesByEntity.get(entity))
394
+ .map((subscribe_id) => {
395
+ const subscribe = this.subscribes.get(subscribe_id)
396
+
397
+ const subscribeSutable = this.checkSubscribeIsSutable(origin, subscribe)
398
+ const documentExistsInSubscribe = this.subscribesByDocument.get(document.name)?.has(subscribe.id)
399
+ const rowSutable = this.checkRowIsSutable(row, subscribe, entity)
400
+
401
+ const sutable = subscribeSutable && (rowSutable || documentExistsInSubscribe)
402
+
403
+ if (sutable) {
404
+ if (this.debug && !rowSutable) {
405
+ console.warn('row is not sutable for subscribe', {
406
+ entity,
407
+ row,
408
+ subscribe,
409
+ subscribeSutable,
410
+ documentExistsInSubscribe,
411
+ rowSutable
412
+ })
413
+ }
320
414
 
321
- switch (action) {
322
- case 'delete': {
323
- this.documentsSubsrcibes.get(document.name)?.forEach((subscribe_id) => {
324
- const subscribe = this.subscribes.get(subscribe_id)
325
- if (subscribe) {
326
- const client = this.clients.get(subscribe.client_id)
327
-
328
- if (client) {
329
- if (this.debug) {
330
- console.log('[CRDT] send action ' + action, subscribe.entity, row.id, subscribe_id)
331
- }
332
-
333
- // todo: send сделать через очередь (с группировкой по клиенту)
334
- client.send(
335
- JSON.stringify({
336
- method: CUBO_CRDT_EVENT.EVENT,
337
- data: {
338
- action,
339
- entity: subscribe.entity,
340
- entity_id: row.id,
341
- subscribe_id: subscribe.id
342
- }
343
- })
344
- )
415
+ let sutableAction = action
416
+ let sutableData: any = data ? Array.from(data as any).slice(0) : undefined
417
+
418
+ if (!rowSutable && action === 'update') {
419
+ if (this.debug) {
420
+ console.log('switch action', action + ' -> delete', { name: document.name, subscribe: subscribe.id })
345
421
  }
346
- }
347
- })
348
- break
349
- }
350
- case 'update':
351
- case 'create': {
352
- this.documentsSubsrcibes.get(document.name)?.forEach((subscribe_id) => {
353
- const subscribe = this.subscribes.get(subscribe_id)
354
-
355
- if (subscribe) {
356
- const client = this.clients.get(subscribe.client_id)
357
-
358
- if (client) {
359
- const noStrategyResolved = !origin?.expose || origin?.expose === 'all'
360
- const strategyOtherResolved = (origin?.expose === 'other' && origin.subscribe_id !== subscribe_id) || false
361
- const strategyClientResolved = origin?.expose === 'client' && subscribe.client_id === origin.client_id
362
- const strategySubscribeResolved = origin?.expose === 'subscribe' && subscribe.id === origin.subscribe_id
363
-
364
- if (noStrategyResolved || strategyOtherResolved || strategyClientResolved || strategySubscribeResolved) {
365
- const isSutable = checkRowIsSutable(document.getJson(), subscribe.filters || {})
366
-
367
- // если документ перестал быть доступным в рамках указанных фильтров - удаляем его
368
- if (!isSutable) {
369
- this.documentUnsubscribe(document.name, subscribe.id)
370
- } else {
371
- if (this.debug) {
372
- console.log('[CRDT] send action ' + action, subscribe.entity, row.id, origin, {
373
- noStrategyResolved,
374
- strategyOtherResolved,
375
- strategyClientResolved,
376
- strategySubscribeResolved
377
- })
378
- }
379
-
380
- // todo: send сделать через очередь (с группировкой по клиенту)
381
- client.send(
382
- JSON.stringify({
383
- method: CUBO_CRDT_EVENT.EVENT,
384
- data: {
385
- action,
386
- entity: subscribe.entity,
387
- entity_id: row.id,
388
- data: Array.from(data as any),
389
- subscribe_id: subscribe.id
390
- }
391
- })
392
- )
393
- }
394
- }
422
+
423
+ sutableAction = 'delete'
424
+ sutableData = null
425
+ } else if (action === 'update' && !documentExistsInSubscribe) {
426
+ if (this.debug) {
427
+ console.log('switch action', 'update -> create', { name: document.name, subscribe: subscribe.id })
395
428
  }
429
+
430
+ sutableAction = 'create'
431
+ sutableData = Array.from(document.stateAsUpdate)
432
+ } else if (action === 'upsert' && !documentExistsInSubscribe) {
433
+ if (this.debug) {
434
+ console.log('switch action', 'upsert -> create', { name: document.name, subscribe: subscribe.id })
435
+ }
436
+
437
+ sutableAction = 'create'
438
+ sutableData = Array.from(document.stateAsUpdate)
439
+ } else if (action === 'delete' && !documentExistsInSubscribe) {
440
+ if (this.debug) {
441
+ console.warn('document to delete is not exist', { name: document.name, subscribe: subscribe.id })
442
+ }
443
+ } else if (action === 'create' && documentExistsInSubscribe) {
444
+ if (this.debug) {
445
+ console.warn('document to create is already exists in subscribe', { name: document.name, subscribe: subscribe.id })
446
+ }
447
+ sutableAction = 'update'
396
448
  }
449
+
450
+ return {
451
+ subscribe,
452
+ action: sutableAction,
453
+ data: sutableData
454
+ }
455
+ }
456
+
457
+ return null
458
+ })
459
+ .filter((i) => !!i) as { subscribe: CuboCrdtServerSubscribe; action: CuboCrdtAction; data: undefined | number[] }[]
460
+
461
+ if (sutableSubscribes.length) {
462
+ const documentSubscribes = this.subscribesByDocument.get(document.name)
463
+
464
+ sutableSubscribes.forEach(({ subscribe, action, data }) => {
465
+ this.sendToClient(subscribe.client_id, {
466
+ action,
467
+ entity: subscribe.entity,
468
+ entity_id: row.id,
469
+ data,
470
+ subscribe_id: subscribe.id
397
471
  })
398
- }
472
+
473
+ switch (action) {
474
+ case 'create':
475
+ case 'upsert':
476
+ case 'update':
477
+ documentSubscribes.add(subscribe.id)
478
+ break
479
+ case 'delete':
480
+ documentSubscribes.delete(subscribe.id)
481
+ this.checkDocumentNeedRemove(document.name)
482
+ break
483
+ }
484
+ })
399
485
  }
400
486
  }
401
487
 
488
+ private async pushSubscribeDocuments(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
489
+ // получаем список всех документов
490
+ const rows = await this.options.fetchRows?.(client, subscribe)
491
+
492
+ // пушим их в сокеты
493
+ for (const row of rows || []) {
494
+ // this.ensureDocument(subscribe.entity as E, row, { subscribes_ids: [subscribe.id] })
495
+ this.ensureDocument(subscribe.entity as E, row)
496
+ }
497
+ }
498
+
499
+ // апгрейд подписки (фильтры поменялись например)
500
+ private async upgradeSubscribe(
501
+ client: WsServerSocket,
502
+ subscribe: CuboCrdtServerSubscribe,
503
+ upgrade: Partial<Pick<CuboCrdtServerSubscribe, 'filters'>>
504
+ ) {
505
+ if (this.subscribesUpgrading.has(subscribe.id)) {
506
+ return this.subscribesUpgrading.get(subscribe.id)
507
+ }
508
+
509
+ this.subscribesUpgrading.set(
510
+ subscribe.id,
511
+ new Promise<void>(async (resolve, reject) => {
512
+ try {
513
+ if (this.debug) {
514
+ console.log('[CRDT] upgrade subscribe ' + client.id, subscribe)
515
+ }
516
+
517
+ if (upgrade.filters) {
518
+ subscribe.filters = cloneDeep(upgrade.filters || {})
519
+ }
520
+
521
+ await this.pushSubscribeDocuments(client, subscribe)
522
+
523
+ resolve()
524
+ } catch (e) {
525
+ if (this.debug) {
526
+ console.error('[CRDT] upgrade subscribe', e)
527
+ }
528
+
529
+ reject(e)
530
+ } finally {
531
+ this.subscribesUpgrading.delete(subscribe.id)
532
+ }
533
+ })
534
+ )
535
+
536
+ return this.subscribesUpgrading.get(subscribe.id)
537
+ }
538
+
539
+ // инциализация подписки
402
540
  private async initSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
403
541
  if (this.subscribesIniting.has(subscribe.id)) {
404
542
  return this.subscribesIniting.get(subscribe.id)
@@ -409,20 +547,15 @@ export class CuboCrdtServer<M, A = {}> {
409
547
  new Promise<void>(async (resolve, reject) => {
410
548
  try {
411
549
  if (this.debug) {
412
- console.log('[CRDT] initSubscribe ' + client.id, subscribe)
550
+ console.log('[CRDT] init subscribe ' + client.id, subscribe)
413
551
  }
414
552
 
415
- // получаем список всех документов
416
- const rows = await this.options.fetchRows?.(client, subscribe)
417
-
418
- for (const row of rows || []) {
419
- this.ensureDocument(subscribe.entity, row, [subscribe.id])
420
- }
553
+ await this.pushSubscribeDocuments(client, subscribe)
421
554
 
422
555
  resolve()
423
556
  } catch (e) {
424
557
  if (this.debug) {
425
- console.error(`[CRDT] initSubscribe`, e)
558
+ console.error('[CRDT] init subscribe', e)
426
559
  }
427
560
 
428
561
  reject(e)
@@ -3,10 +3,12 @@ import { CuboCrdtAction, CuboCrdtExposeStrategy } from '../../types'
3
3
  export type CuboCrdtServerDocumentOptions = {
4
4
  name: string
5
5
  onStore?: (body: object, origin: CuboCrdtServerDocumentOrigin) => void
6
- onCreate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
6
+ // onCreate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
7
7
  onUpdate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
8
8
  // onInit?: (document: CuboCrdtServerDocument) => void
9
9
 
10
+ initialState?: object
11
+
10
12
  yjsOptions?: {
11
13
  guid?: string
12
14
  collectionid?: string
@@ -5,18 +5,21 @@ import { CuboCrdtServerSubscribe } from './subscribe'
5
5
  export * from './document'
6
6
  export * from './subscribe'
7
7
 
8
- export type CuboCrdtServerOptions<M, A> = {
8
+ export type CuboCrdtServerOptions<M, A, E extends Extract<keyof M, string>> = {
9
9
  ws: WsServer
10
- entities: Extract<keyof M, string>[]
10
+ entities: E[]
11
11
  debug?: boolean
12
- fetchRows?: (client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) => Promise<M[Extract<keyof M, string>][]>
13
- storeRow?: <K extends Extract<keyof M, string>>(
12
+ fetchRows?: (client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) => Promise<M[E][]>
13
+ checkRowIsSutable?: (baseState: boolean, ctx: { entity: E; subscribe: CuboCrdtServerSubscribe; row: any }) => boolean
14
+ storeRow?: <K extends E>(
14
15
  entity: K,
15
16
  entity_id: number,
16
17
  row: M[K],
17
18
  opts: {
18
- client: WsServerSocket<{ auth?: A }>
19
+ client: CuboCrdtSocketClient<A>
19
20
  origin: CuboCrdtServerDocumentOrigin
20
21
  }
21
22
  ) => Promise<void> | void
22
23
  }
24
+
25
+ export type CuboCrdtSocketClient<A> = WsServerSocket<{ auth?: A }>
@@ -1,5 +1,5 @@
1
1
  export type CuboCrdtKey<K, M> = K extends Extract<keyof M, string> ? M[K] : any
2
2
 
3
- export type CuboCrdtAction = 'create' | 'update' | 'delete'
3
+ export type CuboCrdtAction = 'create' | 'upsert' | 'update' | 'delete'
4
4
 
5
5
  export type CuboCrdtExposeStrategy = 'all' | 'other' | 'subscribe' | 'client'