@cuboapp/crdt 1.0.5 → 1.0.7
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 +8 -4
- package/src/client/index.ts +381 -72
- package/src/client/types/document.ts +22 -0
- package/src/client/types/index.ts +53 -0
- package/src/client/types/store.ts +15 -0
- package/src/client/types/utils.ts +35 -0
- package/src/constants/index.ts +6 -0
- package/src/index.ts +1 -129
- package/src/server/document/index.ts +84 -0
- package/src/server/index.ts +582 -0
- package/src/server/types/document.ts +56 -0
- package/src/server/types/index.ts +25 -0
- package/src/server/types/subscribe.ts +15 -0
- package/src/types/index.ts +3 -9
- package/src/utils/index.ts +95 -3
- package/src/yjs/index.ts +0 -118
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { cloneDeep } from '@cuboapp/utils'
|
|
2
|
+
import { WsServerSocket } from '@cuboapp/ws'
|
|
3
|
+
|
|
4
|
+
import { CUBO_CRDT_EVENT } from '../constants'
|
|
5
|
+
import { CuboCrdtAction } from '../types'
|
|
6
|
+
import { checkRowIsSutable } from '../utils'
|
|
7
|
+
|
|
8
|
+
import { CuboCrdtClientDocOrigin } from '../client'
|
|
9
|
+
import { CuboCrdtServerDocument } from './document'
|
|
10
|
+
import {
|
|
11
|
+
CuboCrdtServerDocumentIncomingAction,
|
|
12
|
+
CuboCrdtServerDocumentOrigin,
|
|
13
|
+
CuboCrdtServerOptions,
|
|
14
|
+
CuboCrdtServerSubscribe,
|
|
15
|
+
CuboCrdtServerSubscribeDto,
|
|
16
|
+
CuboCrdtServerUnsubscribeDto,
|
|
17
|
+
CuboCrdtSocketClient
|
|
18
|
+
} from './types'
|
|
19
|
+
|
|
20
|
+
export * from './document'
|
|
21
|
+
export * from './types'
|
|
22
|
+
|
|
23
|
+
export class CuboCrdtServer<M, A = {}, E extends Extract<keyof M, string> = Extract<keyof M, string>> {
|
|
24
|
+
constructor(private options: CuboCrdtServerOptions<M, A, E>) {}
|
|
25
|
+
|
|
26
|
+
private clients = new Map<string, CuboCrdtSocketClient<A>>()
|
|
27
|
+
private queue = new Map<string, Set<any>>()
|
|
28
|
+
private sendTimeouts = new Map<string, any>()
|
|
29
|
+
|
|
30
|
+
private subscribes = new Map<string, CuboCrdtServerSubscribe>()
|
|
31
|
+
private subscribesByClient = new Map<string, Set<string>>()
|
|
32
|
+
private subscribesByEntity = new Map<E, Set<string>>()
|
|
33
|
+
private subscribesIniting = new Map<string, Promise<void>>()
|
|
34
|
+
private subscribesUpgrading = new Map<string, Promise<void>>()
|
|
35
|
+
|
|
36
|
+
private documents = new Map<string, CuboCrdtServerDocument>()
|
|
37
|
+
private subscribesByDocument = new Map<string, Set<string>>()
|
|
38
|
+
|
|
39
|
+
public async init() {
|
|
40
|
+
this.entities.forEach((e) => this.subscribesByEntity.set(e, new Set()))
|
|
41
|
+
|
|
42
|
+
this.ws.registerHandler(CUBO_CRDT_EVENT.SUBSCRIBE, async ({ client, message }) => {
|
|
43
|
+
const { subscribe_id: id, entity, filters } = message.data as CuboCrdtServerSubscribeDto
|
|
44
|
+
|
|
45
|
+
if (this.debug) {
|
|
46
|
+
console.log('[CRDT] subscribe', entity, filters)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const subscribe = {
|
|
50
|
+
id,
|
|
51
|
+
client_id: client.id,
|
|
52
|
+
entity,
|
|
53
|
+
filters
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.subscribes.set(id, subscribe)
|
|
57
|
+
|
|
58
|
+
// создаём мапу подписок по клиенту
|
|
59
|
+
this.subscribesByClient.get(client.id)?.add(id)
|
|
60
|
+
|
|
61
|
+
// создаём мапу подписок по сущности
|
|
62
|
+
this.subscribesByEntity.get(entity as E)?.add(id)
|
|
63
|
+
|
|
64
|
+
// инициализируем подписку
|
|
65
|
+
this.initSubscribe(client, subscribe)
|
|
66
|
+
})
|
|
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
|
+
|
|
84
|
+
this.ws.registerHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE, async ({ message }) => {
|
|
85
|
+
const { subscribe_id } = message.data as CuboCrdtServerUnsubscribeDto
|
|
86
|
+
|
|
87
|
+
this.cleanSubscribe(subscribe_id)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
this.ws.registerHandler(CUBO_CRDT_EVENT.EVENT, async ({ message }) => {
|
|
91
|
+
let data = message.data as any
|
|
92
|
+
if (!Array.isArray(data)) {
|
|
93
|
+
data = [data]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.debug) {
|
|
97
|
+
console.log('[CRDT] incoming event', data)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
data.forEach((row: any) => {
|
|
101
|
+
const update = new Uint8Array(row.data)
|
|
102
|
+
|
|
103
|
+
switch (row.action) {
|
|
104
|
+
case 'create':
|
|
105
|
+
this.onDocumentExternalCreate(update, row)
|
|
106
|
+
break
|
|
107
|
+
case 'update':
|
|
108
|
+
this.onDocumentExternalUpdate(update, row)
|
|
109
|
+
break
|
|
110
|
+
case 'delete':
|
|
111
|
+
this.onDocumentExternalDelete(update, row)
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
this.ws.registerHttpHandler('GET', '/stats', () => {
|
|
118
|
+
return {
|
|
119
|
+
clients: this.clients.size,
|
|
120
|
+
subscribes: {
|
|
121
|
+
size: this.subscribes.size,
|
|
122
|
+
byClient: this.subscribesByClient.size,
|
|
123
|
+
byEntity: Array.from(this.subscribesByEntity)
|
|
124
|
+
.map((m) => ({
|
|
125
|
+
entity: m[0],
|
|
126
|
+
size: m[1].size
|
|
127
|
+
}))
|
|
128
|
+
.filter((s) => s.size > 0)
|
|
129
|
+
},
|
|
130
|
+
documents: {
|
|
131
|
+
size: this.documents.size,
|
|
132
|
+
rows: Array.from(this.documents).map((d) => ({
|
|
133
|
+
id: d[0],
|
|
134
|
+
subs: Array.from(this.subscribesByDocument.get(d[0]) || [])
|
|
135
|
+
}))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public async destroy() {
|
|
142
|
+
this.ws.deleteHandler(CUBO_CRDT_EVENT.SUBSCRIBE)
|
|
143
|
+
this.ws.deleteHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE)
|
|
144
|
+
this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
|
|
145
|
+
|
|
146
|
+
this.subscribes.forEach((sub) => this.ws.deleteHandler(sub.id))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public addClient(client: WsServerSocket<{ auth?: A }>) {
|
|
150
|
+
// добавляем клиента
|
|
151
|
+
this.clients.set(client.id, client)
|
|
152
|
+
|
|
153
|
+
// создаем мапу подписок по клиенту
|
|
154
|
+
this.subscribesByClient.set(client.id, new Set())
|
|
155
|
+
|
|
156
|
+
// создаём очередь под клиента
|
|
157
|
+
this.queue.set(client.id, new Set())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public removeClient(client: WsServerSocket) {
|
|
161
|
+
// удаляем все подписки по клиенту
|
|
162
|
+
this.subscribesByClient.get(client.id)?.forEach((subscribe_id) => this.cleanSubscribe(subscribe_id))
|
|
163
|
+
|
|
164
|
+
// удаляем мапу подписок по клиенту
|
|
165
|
+
this.subscribesByClient.delete(client.id)
|
|
166
|
+
|
|
167
|
+
// удаляем очередь под клиента
|
|
168
|
+
this.queue.delete(client.id)
|
|
169
|
+
|
|
170
|
+
// и в принципе клиента
|
|
171
|
+
this.clients.delete(client.id)
|
|
172
|
+
}
|
|
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-хук)
|
|
230
|
+
private cleanSubscribe(subscribe_id: string) {
|
|
231
|
+
const subscribe = this.subscribes.get(subscribe_id)
|
|
232
|
+
if (subscribe) {
|
|
233
|
+
if (this.debug) {
|
|
234
|
+
console.log('[CRDT] unsubscribe', subscribe.entity, subscribe.filters)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// удаляем подписку по сущности
|
|
238
|
+
this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe.id)
|
|
239
|
+
|
|
240
|
+
// отписываем все документы от подписки
|
|
241
|
+
this.subscribesByDocument.forEach((subscribes, docName) => {
|
|
242
|
+
subscribes.delete(subscribe_id)
|
|
243
|
+
|
|
244
|
+
this.checkDocumentNeedRemove(docName)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// удаляем подписки
|
|
248
|
+
this.subscribes.delete(subscribe_id)
|
|
249
|
+
this.subscribesByClient.get(subscribe.client_id)?.delete(subscribe_id)
|
|
250
|
+
this.subscribesByEntity.get(subscribe.entity as E)?.delete(subscribe_id)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public getDocument(entity: string, entity_id: number) {
|
|
255
|
+
return this.documents.get(`${entity}:${entity_id}`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public getSutableSubscribes(entity: string, row: any) {
|
|
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
|
+
})
|
|
270
|
+
.map((m) => m[1])
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public ensureDocument(entity: E, row: any) {
|
|
274
|
+
// todo: врапнуть всё это в Promise
|
|
275
|
+
const entity_id = row.id
|
|
276
|
+
const documentName = `${entity}:${entity_id}`
|
|
277
|
+
|
|
278
|
+
// проверяем документ
|
|
279
|
+
let document = this.documents.get(documentName)
|
|
280
|
+
if (!document) {
|
|
281
|
+
document = new CuboCrdtServerDocument({
|
|
282
|
+
name: documentName,
|
|
283
|
+
initialState: row,
|
|
284
|
+
onUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
|
|
285
|
+
// пушим обновление документа
|
|
286
|
+
// console.log('push document update', document.name)
|
|
287
|
+
|
|
288
|
+
this.pushDocumentAction('update', entity, document!, data, origin)
|
|
289
|
+
},
|
|
290
|
+
onStore: (item: object, origin: CuboCrdtServerDocumentOrigin) => {
|
|
291
|
+
const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
|
|
292
|
+
const client = subscribe && this.clients.get(subscribe.client_id)
|
|
293
|
+
|
|
294
|
+
if (client) {
|
|
295
|
+
return this.options?.storeRow?.(entity as any, entity_id, item as any, { client, origin })
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// создаём документ
|
|
301
|
+
this.documents.set(documentName, document)
|
|
302
|
+
|
|
303
|
+
// создаём пул подписок по документу
|
|
304
|
+
this.subscribesByDocument.set(documentName, new Set())
|
|
305
|
+
|
|
306
|
+
// пушим создание документа
|
|
307
|
+
this.pushDocumentAction('create', entity, document, document.stateAsUpdate)
|
|
308
|
+
|
|
309
|
+
// console.log('create document', documentName)
|
|
310
|
+
} else {
|
|
311
|
+
// console.log('update document', documentName)
|
|
312
|
+
|
|
313
|
+
// пушим обновление документа
|
|
314
|
+
this.pushDocumentAction('upsert', entity, document, document.stateAsUpdate)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return document
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public deleteDocument(entity: E, row: any, origin?: CuboCrdtClientDocOrigin) {
|
|
321
|
+
if (this.debug) {
|
|
322
|
+
console.log('[CRDT] delete document', entity, row.id, origin)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const documentName = `${entity}:${row.id}`
|
|
326
|
+
const document = this.documents.get(documentName)
|
|
327
|
+
|
|
328
|
+
if (document) {
|
|
329
|
+
// сначала пушим удаление документа
|
|
330
|
+
this.pushDocumentAction('delete', entity, document, null, origin)
|
|
331
|
+
|
|
332
|
+
// удаляем документ
|
|
333
|
+
this.documents.delete(documentName)
|
|
334
|
+
|
|
335
|
+
// дестроим документ
|
|
336
|
+
document.destroy()
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private onDocumentExternalCreate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
|
|
341
|
+
console.warn('[CRDT] onDocumentExternalCreate not implemented')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private onDocumentExternalUpdate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
|
|
345
|
+
const document = this.getDocument(action.entity, action.entity_id)
|
|
346
|
+
|
|
347
|
+
// console.log('onDocumentExternalUpdate', action)
|
|
348
|
+
|
|
349
|
+
if (!document) {
|
|
350
|
+
console.warn('[CRDT] onDocumentExternalUpdate - document not found:', action.entity, action.entity_id)
|
|
351
|
+
} else {
|
|
352
|
+
document.applyUpdate(new Uint8Array(update), action.origin)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private onDocumentExternalDelete(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
|
|
357
|
+
console.warn('[CRDT] onDocumentExternalDelete not implemented')
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private checkSubscribeIsSutable(origin: CuboCrdtServerDocumentOrigin, subscribe?: CuboCrdtServerSubscribe) {
|
|
361
|
+
if (!subscribe) {
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
364
|
+
|
|
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
|
|
369
|
+
|
|
370
|
+
return noStrategyResolved || strategyOtherResolved || strategyClientResolved || strategySubscribeResolved
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private checkRowIsSutable(row: any, subscribe: CuboCrdtServerSubscribe, entity: E) {
|
|
374
|
+
const baseState = checkRowIsSutable(row, subscribe.filters || {})
|
|
375
|
+
|
|
376
|
+
if (!this.options.checkRowIsSutable) {
|
|
377
|
+
return baseState
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return this.options.checkRowIsSutable(baseState, { entity, subscribe, row: row as any })
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private pushDocumentAction(
|
|
384
|
+
action: CuboCrdtAction,
|
|
385
|
+
entity: E,
|
|
386
|
+
document: CuboCrdtServerDocument,
|
|
387
|
+
data?: Uint8Array | null,
|
|
388
|
+
origin?: CuboCrdtServerDocumentOrigin
|
|
389
|
+
) {
|
|
390
|
+
const row = document.getJson()
|
|
391
|
+
|
|
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
|
+
}
|
|
414
|
+
|
|
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 })
|
|
421
|
+
}
|
|
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 })
|
|
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'
|
|
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
|
|
471
|
+
})
|
|
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
|
+
})
|
|
485
|
+
}
|
|
486
|
+
}
|
|
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
|
+
// инциализация подписки
|
|
540
|
+
private async initSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
|
|
541
|
+
if (this.subscribesIniting.has(subscribe.id)) {
|
|
542
|
+
return this.subscribesIniting.get(subscribe.id)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
this.subscribesIniting.set(
|
|
546
|
+
subscribe.id,
|
|
547
|
+
new Promise<void>(async (resolve, reject) => {
|
|
548
|
+
try {
|
|
549
|
+
if (this.debug) {
|
|
550
|
+
console.log('[CRDT] init subscribe ' + client.id, subscribe)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
await this.pushSubscribeDocuments(client, subscribe)
|
|
554
|
+
|
|
555
|
+
resolve()
|
|
556
|
+
} catch (e) {
|
|
557
|
+
if (this.debug) {
|
|
558
|
+
console.error('[CRDT] init subscribe', e)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
reject(e)
|
|
562
|
+
} finally {
|
|
563
|
+
this.subscribesIniting.delete(subscribe.id)
|
|
564
|
+
}
|
|
565
|
+
})
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return this.subscribesIniting.get(subscribe.id)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private get debug() {
|
|
572
|
+
return this.options.debug
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private get ws() {
|
|
576
|
+
return this.options.ws
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private get entities() {
|
|
580
|
+
return this.options.entities
|
|
581
|
+
}
|
|
582
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CuboCrdtAction, CuboCrdtExposeStrategy } from '../../types'
|
|
2
|
+
|
|
3
|
+
export type CuboCrdtServerDocumentOptions = {
|
|
4
|
+
name: string
|
|
5
|
+
onStore?: (body: object, origin: CuboCrdtServerDocumentOrigin) => void
|
|
6
|
+
// onCreate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
|
|
7
|
+
onUpdate?: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => void
|
|
8
|
+
// onInit?: (document: CuboCrdtServerDocument) => void
|
|
9
|
+
|
|
10
|
+
initialState?: object
|
|
11
|
+
|
|
12
|
+
yjsOptions?: {
|
|
13
|
+
guid?: string
|
|
14
|
+
collectionid?: string
|
|
15
|
+
gc?: boolean
|
|
16
|
+
gcFilter?: () => true
|
|
17
|
+
meta?: any
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CuboCrdtServerDocumentIncomingAction = {
|
|
22
|
+
// действие
|
|
23
|
+
action: CuboCrdtAction
|
|
24
|
+
|
|
25
|
+
// сущность
|
|
26
|
+
entity: string
|
|
27
|
+
|
|
28
|
+
// ид сущности
|
|
29
|
+
entity_id: number
|
|
30
|
+
|
|
31
|
+
// контент апдейта
|
|
32
|
+
data: number[]
|
|
33
|
+
|
|
34
|
+
// ориджин
|
|
35
|
+
origin: CuboCrdtServerDocumentOrigin
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type CuboCrdtServerDocumentOrigin = {
|
|
39
|
+
// сохранять ли на бэке в дебаунсе
|
|
40
|
+
store?: boolean
|
|
41
|
+
|
|
42
|
+
// реагировать ли в onUpdate (на бэке)
|
|
43
|
+
react?: boolean
|
|
44
|
+
|
|
45
|
+
// обновлённые ключи
|
|
46
|
+
keys?: string[]
|
|
47
|
+
|
|
48
|
+
// куда раскатывать обновления - всем или всем кроме себя
|
|
49
|
+
expose?: CuboCrdtExposeStrategy
|
|
50
|
+
|
|
51
|
+
// id подписки-исходника
|
|
52
|
+
subscribe_id?: string
|
|
53
|
+
|
|
54
|
+
// id клиента
|
|
55
|
+
client_id?: string
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WsServer, WsServerSocket } from '@cuboapp/ws'
|
|
2
|
+
import { CuboCrdtServerDocumentOrigin } from './document'
|
|
3
|
+
import { CuboCrdtServerSubscribe } from './subscribe'
|
|
4
|
+
|
|
5
|
+
export * from './document'
|
|
6
|
+
export * from './subscribe'
|
|
7
|
+
|
|
8
|
+
export type CuboCrdtServerOptions<M, A, E extends Extract<keyof M, string>> = {
|
|
9
|
+
ws: WsServer
|
|
10
|
+
entities: E[]
|
|
11
|
+
debug?: boolean
|
|
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>(
|
|
15
|
+
entity: K,
|
|
16
|
+
entity_id: number,
|
|
17
|
+
row: M[K],
|
|
18
|
+
opts: {
|
|
19
|
+
client: CuboCrdtSocketClient<A>
|
|
20
|
+
origin: CuboCrdtServerDocumentOrigin
|
|
21
|
+
}
|
|
22
|
+
) => Promise<void> | void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type CuboCrdtSocketClient<A> = WsServerSocket<{ auth?: A }>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type CuboCrdtServerUnsubscribeDto = {
|
|
2
|
+
subscribe_id: string
|
|
3
|
+
}
|
|
4
|
+
export type CuboCrdtServerSubscribeDto = {
|
|
5
|
+
subscribe_id: string
|
|
6
|
+
entity: string
|
|
7
|
+
filters: {
|
|
8
|
+
[K in 'id' | string]: any
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type CuboCrdtServerSubscribe = {
|
|
13
|
+
id: string
|
|
14
|
+
client_id: string
|
|
15
|
+
} & Pick<CuboCrdtServerSubscribeDto, 'entity' | 'filters'>
|
package/src/types/index.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
export type CuboCrdtKey<K, M> = K extends Extract<keyof M, string> ? M[K] : any
|
|
2
2
|
|
|
3
|
-
export type
|
|
4
|
-
host?: string
|
|
5
|
-
port?: number
|
|
6
|
-
debug?: boolean
|
|
3
|
+
export type CuboCrdtAction = 'create' | 'upsert' | 'update' | 'delete'
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
beforeLoadDocument?: (name: string) => Promise<any> | any
|
|
11
|
-
}
|
|
5
|
+
export type CuboCrdtExposeStrategy = 'all' | 'other' | 'subscribe' | 'client'
|