@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.
- package/package.json +1 -1
- package/src/client/index.ts +57 -26
- package/src/client/types/index.ts +4 -2
- package/src/constants/index.ts +1 -0
- package/src/server/document/index.ts +14 -21
- package/src/server/index.ts +297 -164
- package/src/server/types/document.ts +3 -1
- package/src/server/types/index.ts +8 -5
- package/src/types/index.ts +1 -1
- package/src/utils/index.ts +54 -24
- package/src/client/old.ts +0 -249
- package/src/server/old/document/index.ts +0 -107
- package/src/server/old/index.ts +0 -49
package/src/server/index.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
27
|
-
private subscribesByEntity = new Map<
|
|
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
|
|
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
|
|
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)?.
|
|
59
|
+
this.subscribesByClient.get(client.id)?.add(id)
|
|
54
60
|
|
|
55
61
|
// создаём мапу подписок по сущности
|
|
56
|
-
this.subscribesByEntity.get(entity)?.
|
|
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.
|
|
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
|
|
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((
|
|
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.
|
|
157
|
-
|
|
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
|
-
|
|
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) =>
|
|
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:
|
|
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
|
-
|
|
196
|
-
this.pushDocumentAction('create', document!, data, origin)
|
|
197
|
-
},
|
|
283
|
+
initialState: row,
|
|
198
284
|
onUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
199
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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.
|
|
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
|
-
|
|
232
|
-
})
|
|
313
|
+
// пушим обновление документа
|
|
314
|
+
this.pushDocumentAction('upsert', entity, document, document.stateAsUpdate)
|
|
233
315
|
}
|
|
234
316
|
|
|
235
|
-
return
|
|
317
|
+
return document
|
|
236
318
|
}
|
|
237
319
|
|
|
238
|
-
public deleteDocument(entity:
|
|
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,
|
|
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
|
|
282
|
-
|
|
360
|
+
private checkSubscribeIsSutable(origin: CuboCrdtServerDocumentOrigin, subscribe?: CuboCrdtServerSubscribe) {
|
|
361
|
+
if (!subscribe) {
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
283
364
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
291
|
-
|
|
370
|
+
return noStrategyResolved || strategyOtherResolved || strategyClientResolved || strategySubscribeResolved
|
|
371
|
+
}
|
|
292
372
|
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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]
|
|
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(
|
|
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:
|
|
10
|
+
entities: E[]
|
|
11
11
|
debug?: boolean
|
|
12
|
-
fetchRows?: (client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) => Promise<M[
|
|
13
|
-
|
|
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:
|
|
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 }>
|
package/src/types/index.ts
CHANGED
|
@@ -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'
|