@cuboapp/crdt 1.0.4 → 1.0.6

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.
@@ -0,0 +1,449 @@
1
+ import { WsServerSocket } from '@cuboapp/ws'
2
+
3
+ import { CUBO_CRDT_EVENT } from '../constants'
4
+ import { CuboCrdtAction } from '../types'
5
+ import { checkRowIsSutable } from '../utils'
6
+
7
+ import { CuboCrdtServerDocument } from './document'
8
+ import {
9
+ CuboCrdtServerDocumentIncomingAction,
10
+ CuboCrdtServerDocumentOrigin,
11
+ CuboCrdtServerOptions,
12
+ CuboCrdtServerSubscribe,
13
+ CuboCrdtServerSubscribeDto,
14
+ CuboCrdtServerUnsubscribeDto
15
+ } from './types'
16
+
17
+ export * from './document'
18
+ export * from './types'
19
+
20
+ export class CuboCrdtServer<M, A = {}> {
21
+ constructor(private options: CuboCrdtServerOptions<M, A>) {}
22
+
23
+ private clients = new Map<string, WsServerSocket<{ auth?: A }>>()
24
+
25
+ private subscribes = new Map<string, CuboCrdtServerSubscribe>()
26
+ private subscribesByClient = new Map<string, Map<string, CuboCrdtServerSubscribe>>()
27
+ private subscribesByEntity = new Map<string, Map<string, CuboCrdtServerSubscribe>>()
28
+ private subscribesIniting = new Map<string, Promise<void>>()
29
+
30
+ private documents = new Map<string, CuboCrdtServerDocument>()
31
+ private documentsSubsrcibes = new Map<string, Set<string>>()
32
+
33
+ public async init() {
34
+ this.entities.forEach((e) => this.subscribesByEntity.set(e, new Map()))
35
+
36
+ this.ws.registerHandler(CUBO_CRDT_EVENT.SUBSCRIBE, async ({ client, message }) => {
37
+ const { subscribe_id: id, entity, filters } = message.data as CuboCrdtServerSubscribeDto
38
+
39
+ if (this.debug) {
40
+ console.log('[CRDT] subscribe', entity, filters)
41
+ }
42
+
43
+ const subscribe = {
44
+ id,
45
+ client_id: client.id,
46
+ entity,
47
+ filters
48
+ }
49
+
50
+ this.subscribes.set(id, subscribe)
51
+
52
+ // создаём мапу подписок по клиенту
53
+ this.subscribesByClient.get(client.id)?.set(id, subscribe)
54
+
55
+ // создаём мапу подписок по сущности
56
+ this.subscribesByEntity.get(entity)?.set(id, subscribe)
57
+
58
+ this.initSubscribe(client, subscribe)
59
+ })
60
+
61
+ this.ws.registerHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE, async ({ message }) => {
62
+ const { subscribe_id } = message.data as CuboCrdtServerUnsubscribeDto
63
+
64
+ this.cleanSubscribe(subscribe_id)
65
+ })
66
+
67
+ this.ws.registerHandler(CUBO_CRDT_EVENT.EVENT, async ({ message }) => {
68
+ let data = message.data as any
69
+ if (!Array.isArray(data)) {
70
+ data = [data]
71
+ }
72
+
73
+ if (this.debug) {
74
+ console.log('[CRDT] incoming event', data)
75
+ }
76
+
77
+ data.forEach((row: any) => {
78
+ const update = new Uint8Array(row.data)
79
+
80
+ switch (row.action) {
81
+ case 'create':
82
+ this.onDocumentExternalCreate(update, row)
83
+ break
84
+ case 'update':
85
+ this.onDocumentExternalUpdate(update, row)
86
+ break
87
+ case 'delete':
88
+ this.onDocumentExternalDelete(update, row)
89
+ break
90
+ }
91
+ })
92
+ })
93
+
94
+ this.ws.registerHttpHandler('GET', '/stats', () => {
95
+ return {
96
+ clients: this.clients.size,
97
+ subscribes: {
98
+ size: this.subscribes.size,
99
+ byClient: this.subscribesByClient.size,
100
+ byEntity: Array.from(this.subscribesByEntity)
101
+ .map((m) => ({
102
+ entity: m[0],
103
+ size: m[1].size
104
+ }))
105
+ .filter((s) => s.size > 0)
106
+ },
107
+ documents: {
108
+ size: this.documents.size,
109
+ rows: Array.from(this.documents).map((d) => ({
110
+ id: d[0],
111
+ subs: Array.from(this.documentsSubsrcibes.get(d[0]) || [])
112
+ }))
113
+ }
114
+ }
115
+ })
116
+ }
117
+
118
+ public async destroy() {
119
+ this.ws.deleteHandler(CUBO_CRDT_EVENT.SUBSCRIBE)
120
+ this.ws.deleteHandler(CUBO_CRDT_EVENT.UNSUBSCRIBE)
121
+ this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
122
+
123
+ this.subscribes.forEach((sub) => this.ws.deleteHandler(sub.id))
124
+ }
125
+
126
+ public addClient(client: WsServerSocket<{ auth?: A }>) {
127
+ // добавляем клиента
128
+ this.clients.set(client.id, client)
129
+
130
+ // создаем мапу подписок по клиенту
131
+ this.subscribesByClient.set(client.id, new Map())
132
+ }
133
+
134
+ public removeClient(client: WsServerSocket) {
135
+ // удаляем все подписки по клиенту
136
+ this.subscribesByClient.get(client.id)?.forEach((sub) => this.cleanSubscribe(sub.id))
137
+
138
+ // удаляем мапу подписок по клиенту
139
+ this.subscribesByClient.delete(client.id)
140
+
141
+ // и в принципе клиента
142
+ this.clients.delete(client.id)
143
+ }
144
+
145
+ private cleanSubscribe(subscribe_id: string) {
146
+ const subscribe = this.subscribes.get(subscribe_id)
147
+ if (subscribe) {
148
+ if (this.debug) {
149
+ console.log('[CRDT] unsubscribe', subscribe.entity, subscribe.filters)
150
+ }
151
+
152
+ // удаляем подписку по сущности
153
+ this.subscribesByEntity.get(subscribe.entity)?.delete(subscribe.id)
154
+
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
+ }
163
+
164
+ this.documents.get(docName)?.destroy()
165
+ this.documents.delete(docName)
166
+ }
167
+ })
168
+
169
+ // удаляем подписки
170
+ this.subscribes.delete(subscribe_id)
171
+ this.subscribesByClient.get(subscribe.client_id)?.delete(subscribe_id)
172
+ this.subscribesByEntity.get(subscribe.entity)?.delete(subscribe_id)
173
+ }
174
+ }
175
+
176
+ public getDocument(entity: string, entity_id: number) {
177
+ return this.documents.get(`${entity}:${entity_id}`)
178
+ }
179
+
180
+ public getSutableSubscribes(entity: string, row: any) {
181
+ return Array.from(this.subscribesByEntity.get(entity) || [])
182
+ .filter((m) => checkRowIsSutable(row, m[1].filters))
183
+ .map((m) => m[1])
184
+ }
185
+
186
+ public ensureDocument(entity: string, row: any, subscribes_ids?: string[]) {
187
+ const entity_id = row.id
188
+ const documentName = `${entity}:${entity_id}`
189
+
190
+ // проверяем документ
191
+ let document = this.documents.get(documentName)
192
+ if (!document) {
193
+ document = new CuboCrdtServerDocument({
194
+ name: documentName,
195
+ onCreate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
196
+ this.pushDocumentAction('create', document!, data, origin)
197
+ },
198
+ onUpdate: (data: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) => {
199
+ this.pushDocumentAction('update', document!, data, origin)
200
+ },
201
+ onStore: (item: object, origin: CuboCrdtServerDocumentOrigin) => {
202
+ const subscribe = origin.subscribe_id && this.subscribes.get(origin.subscribe_id)
203
+ const client = subscribe && this.clients.get(subscribe.client_id)
204
+
205
+ if (client) {
206
+ return this.options?.storeRow?.(entity as any, entity_id, item as any, { client, origin })
207
+ }
208
+ }
209
+ })
210
+
211
+ this.documents.set(documentName, document)
212
+
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
+ }
221
+
222
+ // инициализируем документ
223
+ document.init(row)
224
+ } else {
225
+ // добавляем подписки к документу
226
+ const subs = this.documentsSubsrcibes.get(documentName)
227
+ subscribes_ids?.forEach((subscribe_id) => {
228
+ subs?.add(subscribe_id)
229
+
230
+ // пушим документ в сокеты (в текущем состоянии)
231
+ this.pushDocumentAction('update', document!, document!.stateAsUpdate, { subscribe_id, expose: 'subscribe' })
232
+ })
233
+ }
234
+
235
+ return this.documents.get(documentName)
236
+ }
237
+
238
+ public deleteDocument(entity: string, row: any) {
239
+ if (this.debug) {
240
+ console.log('[CRDT] delete document', entity, row.id)
241
+ }
242
+
243
+ const documentName = `${entity}:${row.id}`
244
+ const document = this.documents.get(documentName)
245
+
246
+ if (document) {
247
+ // отписываемся от всех подписок на документ
248
+ this.pushDocumentAction('delete', document, null, { expose: 'subscribe' })
249
+
250
+ // удаляем все подписки по документу
251
+ this.documentsSubsrcibes.delete(documentName)
252
+
253
+ // удаляем документ
254
+ this.documents.delete(documentName)
255
+
256
+ // дестроим документ
257
+ document.destroy()
258
+ }
259
+ }
260
+
261
+ private onDocumentExternalCreate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
262
+ console.warn('[CRDT] onDocumentExternalCreate not implemented')
263
+ }
264
+
265
+ private onDocumentExternalUpdate(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
266
+ const document = this.getDocument(action.entity, action.entity_id)
267
+
268
+ // console.log('onDocumentExternalUpdate', action)
269
+
270
+ if (!document) {
271
+ console.warn('[CRDT] onDocumentExternalUpdate - document not found:', action.entity, action.entity_id)
272
+ } else {
273
+ document.applyUpdate(new Uint8Array(update), action.origin)
274
+ }
275
+ }
276
+
277
+ private onDocumentExternalDelete(update: Uint8Array, action: CuboCrdtServerDocumentIncomingAction) {
278
+ console.warn('[CRDT] onDocumentExternalDelete not implemented')
279
+ }
280
+
281
+ private documentUnsubscribe(docName: string, subscribe_id: string, push = true) {
282
+ const document = this.documents.get(docName)
283
+
284
+ if (document) {
285
+ // сначала отсылаем, затем удаляем подписки (иначе не будет куда слать)
286
+ if (push) {
287
+ this.pushDocumentAction('delete', document, null, { subscribe_id, expose: 'subscribe' })
288
+ }
289
+
290
+ const documentSubscribes = this.documentsSubsrcibes.get(docName)
291
+ documentSubscribes?.delete(subscribe_id)
292
+
293
+ // если нет больше подписок - удаляем документ
294
+ if (!documentSubscribes?.size) {
295
+ if (this.debug) {
296
+ console.log('[CRDT] delete document', name)
297
+ }
298
+
299
+ document?.destroy()
300
+ this.documents.delete(document.name)
301
+ }
302
+ }
303
+ }
304
+
305
+ private pushDocumentAction(
306
+ action: CuboCrdtAction,
307
+ document: CuboCrdtServerDocument,
308
+ data?: Uint8Array | null,
309
+ origin?: CuboCrdtServerDocumentOrigin
310
+ ) {
311
+ const row = document.getJson()
312
+
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
+ }
320
+
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
+ )
345
+ }
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
+ }
395
+ }
396
+ }
397
+ })
398
+ }
399
+ }
400
+ }
401
+
402
+ private async initSubscribe(client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) {
403
+ if (this.subscribesIniting.has(subscribe.id)) {
404
+ return this.subscribesIniting.get(subscribe.id)
405
+ }
406
+
407
+ this.subscribesIniting.set(
408
+ subscribe.id,
409
+ new Promise<void>(async (resolve, reject) => {
410
+ try {
411
+ if (this.debug) {
412
+ console.log('[CRDT] initSubscribe ' + client.id, subscribe)
413
+ }
414
+
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
+ }
421
+
422
+ resolve()
423
+ } catch (e) {
424
+ if (this.debug) {
425
+ console.error(`[CRDT] initSubscribe`, e)
426
+ }
427
+
428
+ reject(e)
429
+ } finally {
430
+ this.subscribesIniting.delete(subscribe.id)
431
+ }
432
+ })
433
+ )
434
+
435
+ return this.subscribesIniting.get(subscribe.id)
436
+ }
437
+
438
+ private get debug() {
439
+ return this.options.debug
440
+ }
441
+
442
+ private get ws() {
443
+ return this.options.ws
444
+ }
445
+
446
+ private get entities() {
447
+ return this.options.entities
448
+ }
449
+ }
@@ -0,0 +1,107 @@
1
+ import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'
2
+
3
+ export class CuboCrdtDocument {
4
+ private ydoc: Doc
5
+ private subs = new Map<string, (data: Uint8Array<ArrayBufferLike>, origin?: any) => void>()
6
+
7
+ constructor(
8
+ public opts: {
9
+ name: string
10
+ initialState?: any
11
+ autoRemove?: boolean
12
+ onDelete?: () => void
13
+ yjsOptions?: {
14
+ guid?: string
15
+ collectionid?: string
16
+ gc?: boolean
17
+ gcFilter?: () => true
18
+ meta?: any
19
+ }
20
+ }
21
+ ) {}
22
+
23
+ get stateAsUpdate() {
24
+ return encodeStateAsUpdate(this.ydoc)
25
+ }
26
+
27
+ async init() {
28
+ this.ydoc = new Doc({
29
+ ...(this.opts.yjsOptions || {}),
30
+ autoLoad: false
31
+ })
32
+
33
+ const state = this.opts?.initialState ?? {}
34
+
35
+ return new Promise<Uint8Array<ArrayBufferLike>>((resolve) => {
36
+ this.ydoc.once('update', () => {
37
+ this.ydoc.on('update', (data, origin) => {
38
+ this.subs.forEach((sub) => sub(data, origin))
39
+ })
40
+
41
+ resolve(this.stateAsUpdate)
42
+ })
43
+
44
+ const map = this.getMap()
45
+
46
+ this.ydoc.transact(() => {
47
+ if (Object.keys(state).length) {
48
+ Object.entries(state).forEach(([key, value]) => {
49
+ map.set(key, value)
50
+ })
51
+ }
52
+
53
+ this.getState().setAttribute('inited', 'Y')
54
+ })
55
+ })
56
+ }
57
+
58
+ get inited() {
59
+ return this.getState().getAttribute('inited') === 'Y'
60
+ }
61
+
62
+ get id() {
63
+ return this.ydoc.guid
64
+ }
65
+
66
+ applyUpdate(update: Uint8Array, origin?: any) {
67
+ return applyUpdate(this.ydoc, update, origin)
68
+ }
69
+
70
+ subscribe(id: string, cb: (data: Uint8Array<ArrayBufferLike>, origin?: any) => void) {
71
+ this.subs.set(id, cb)
72
+ }
73
+
74
+ unsubscribe(id: string) {
75
+ this.subs.delete(id)
76
+
77
+ if (!this.subs.size && this.opts?.autoRemove !== false) {
78
+ this.opts?.onDelete?.()
79
+ }
80
+ }
81
+
82
+ getState() {
83
+ return this.ydoc.getText('state')
84
+ }
85
+
86
+ getMap() {
87
+ return this.ydoc.getMap()
88
+ }
89
+
90
+ getXmlFragment() {
91
+ return this.ydoc.getXmlFragment()
92
+ }
93
+
94
+ async writeMap(dto: object, origin?: any) {
95
+ return new Promise<void>(async (resolve) => {
96
+ const map = this.getMap()
97
+
98
+ this.ydoc.transact(() => {
99
+ Object.entries(dto).forEach(([key, value]) => {
100
+ map.set(key, value)
101
+ })
102
+
103
+ resolve()
104
+ }, origin)
105
+ })
106
+ }
107
+ }
@@ -0,0 +1,49 @@
1
+ import { CuboCrdtDocument } from './document'
2
+
3
+ export * from './document'
4
+
5
+ export class CuboCrdtServer<T> {
6
+ constructor(private options: { debug?: boolean }) {}
7
+
8
+ public documents = new Map<string, CuboCrdtDocument>()
9
+ private loading = new Map<string, Promise<CuboCrdtDocument>>()
10
+
11
+ public async getDocument(name: string, opts?: { autoCreate?: boolean; initialState?: any }) {
12
+ // существующий документ
13
+ if (!this.documents.has(name)) {
14
+ if (!this.loading.has(name) && opts?.autoCreate !== false) {
15
+ if (this.options.debug) {
16
+ console.log('CRDT create document load start', name)
17
+ }
18
+
19
+ this.loading.set(
20
+ name,
21
+ new Promise(async (resolve) => {
22
+ const document = new CuboCrdtDocument({
23
+ name,
24
+ initialState: opts?.initialState,
25
+ autoRemove: true,
26
+ onDelete: () => {
27
+ this.documents.delete(name)
28
+ }
29
+ })
30
+
31
+ await document.init()
32
+
33
+ resolve(document)
34
+ })
35
+ )
36
+ }
37
+
38
+ const document = await this.loading.get(name)
39
+
40
+ this.loading.delete(name)
41
+
42
+ if (document) {
43
+ this.documents.set(name, document)
44
+ }
45
+ }
46
+
47
+ return this.documents.get(name)
48
+ }
49
+ }
@@ -0,0 +1,54 @@
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
+ yjsOptions?: {
11
+ guid?: string
12
+ collectionid?: string
13
+ gc?: boolean
14
+ gcFilter?: () => true
15
+ meta?: any
16
+ }
17
+ }
18
+
19
+ export type CuboCrdtServerDocumentIncomingAction = {
20
+ // действие
21
+ action: CuboCrdtAction
22
+
23
+ // сущность
24
+ entity: string
25
+
26
+ // ид сущности
27
+ entity_id: number
28
+
29
+ // контент апдейта
30
+ data: number[]
31
+
32
+ // ориджин
33
+ origin: CuboCrdtServerDocumentOrigin
34
+ }
35
+
36
+ export type CuboCrdtServerDocumentOrigin = {
37
+ // сохранять ли на бэке в дебаунсе
38
+ store?: boolean
39
+
40
+ // реагировать ли в onUpdate (на бэке)
41
+ react?: boolean
42
+
43
+ // обновлённые ключи
44
+ keys?: string[]
45
+
46
+ // куда раскатывать обновления - всем или всем кроме себя
47
+ expose?: CuboCrdtExposeStrategy
48
+
49
+ // id подписки-исходника
50
+ subscribe_id?: string
51
+
52
+ // id клиента
53
+ client_id?: string
54
+ }
@@ -0,0 +1,22 @@
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> = {
9
+ ws: WsServer
10
+ entities: Extract<keyof M, string>[]
11
+ debug?: boolean
12
+ fetchRows?: (client: WsServerSocket, subscribe: CuboCrdtServerSubscribe) => Promise<M[Extract<keyof M, string>][]>
13
+ storeRow?: <K extends Extract<keyof M, string>>(
14
+ entity: K,
15
+ entity_id: number,
16
+ row: M[K],
17
+ opts: {
18
+ client: WsServerSocket<{ auth?: A }>
19
+ origin: CuboCrdtServerDocumentOrigin
20
+ }
21
+ ) => Promise<void> | void
22
+ }
@@ -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'>