@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cuboapp/crdt",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "CRDT for CUBO",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"repository": "git@github.com:cuboapp/crdt.git",
|
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@cuboapp/types": "^2.0.
|
|
15
|
-
"@cuboapp/utils": "1.0.
|
|
16
|
-
"@cuboapp/ws": "1.0.
|
|
14
|
+
"@cuboapp/types": "^2.0.12",
|
|
15
|
+
"@cuboapp/utils": "1.0.10",
|
|
16
|
+
"@cuboapp/ws": "1.0.5",
|
|
17
17
|
"ws": "^8.19.0",
|
|
18
18
|
"yjs": "^13.6.29"
|
|
19
19
|
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"vue": "^3.5.26",
|
|
22
|
+
"vue-router": "^4.6.3"
|
|
23
|
+
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"@types/node": "^24.10.1",
|
|
22
26
|
"@types/ws": "^8.18.1",
|
package/src/client/index.ts
CHANGED
|
@@ -1,106 +1,415 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { keyBy, pick, uuid } from '@cuboapp/utils'
|
|
2
|
+
import { computed, reactive } from 'vue'
|
|
3
3
|
import { applyUpdate, Doc } from 'yjs'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
protocol?: 'wss' | 'ws' | string
|
|
9
|
-
authToken?: () => Promise<string>
|
|
10
|
-
debug?: boolean
|
|
11
|
-
}
|
|
5
|
+
import { CUBO_CRDT_EVENT } from '../constants'
|
|
6
|
+
import { CuboCrdtServerDocumentIncomingAction } from '../server'
|
|
7
|
+
import { CuboCrdtKey } from '../types'
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
import {
|
|
10
|
+
CuboCrdtClientDocOrigin,
|
|
11
|
+
CuboCrdtClientDocUpdateOptions,
|
|
12
|
+
CuboCrdtClientList,
|
|
13
|
+
CuboCrdtClientOptions,
|
|
14
|
+
CuboCrdtClientRow,
|
|
15
|
+
CuboCrdtClientStore,
|
|
16
|
+
CuboCrdtClientSubscribeEvent,
|
|
17
|
+
CuboCrdtClientUseOptions
|
|
18
|
+
} from './types'
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
export * from './types'
|
|
21
|
+
|
|
22
|
+
export class CuboCrdtClient<M> {
|
|
23
|
+
public store: CuboCrdtClientStore<M> = {}
|
|
24
|
+
|
|
25
|
+
private listeners: Map<string, Map<string, (ctx: CuboCrdtClientSubscribeEvent) => void>> = new Map()
|
|
26
|
+
|
|
27
|
+
constructor(private opts: CuboCrdtClientOptions<M>) {
|
|
28
|
+
opts.entities.forEach((e) => this.listeners.set(e, new Map()))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async start() {
|
|
32
|
+
this.ws.registerHandler(CUBO_CRDT_EVENT.EVENT, ({ message }) => {
|
|
33
|
+
let events: CuboCrdtClientSubscribeEvent[] = message.data as any
|
|
34
|
+
if (!Array.isArray(events)) {
|
|
35
|
+
events = [events as any]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// todo: perform events through queue in await mode
|
|
39
|
+
events.forEach((event) => {
|
|
40
|
+
const listeners = this.listeners.get(event.entity)
|
|
41
|
+
|
|
42
|
+
if (this.debug) {
|
|
43
|
+
console.log('[CRDT] incoming event', { listeners: listeners?.size, event })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (event.subscribe_id) {
|
|
47
|
+
const listener = listeners?.get(event.subscribe_id)
|
|
48
|
+
if (!listener) {
|
|
49
|
+
console.warn('[CRDT] listener not found:', event.subscribe_id, event)
|
|
50
|
+
} else {
|
|
51
|
+
listener(event)
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
listeners?.forEach((cb) => cb(event))
|
|
55
|
+
}
|
|
56
|
+
})
|
|
27
57
|
})
|
|
28
58
|
}
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
async stop() {
|
|
61
|
+
this.ws.deleteHandler(CUBO_CRDT_EVENT.EVENT)
|
|
32
62
|
|
|
33
|
-
this.
|
|
63
|
+
this.listeners.clear()
|
|
34
64
|
}
|
|
35
65
|
|
|
36
|
-
public
|
|
37
|
-
|
|
38
|
-
await this.ws.disconnect()
|
|
39
|
-
} catch {}
|
|
66
|
+
public useComputedList<K extends Extract<keyof M, string>>(entity: K) {
|
|
67
|
+
return computed(() => (this.store[entity]?.state.rows || []) as M[K][])
|
|
40
68
|
}
|
|
41
69
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
}
|
|
70
|
+
public useComputedRow<K extends Extract<keyof M, string>>(entity: K, entity_id: number) {
|
|
71
|
+
return computed(() => {
|
|
72
|
+
return ((this.store[entity]?.state.rows || []) as M[K][]).find((row: any) => row.id === entity_id)
|
|
73
|
+
})
|
|
46
74
|
}
|
|
47
75
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
public useList<K extends Extract<keyof M, string>, T = CuboCrdtKey<K, M>>(
|
|
77
|
+
entity: K | string,
|
|
78
|
+
opts?: CuboCrdtClientUseOptions
|
|
79
|
+
): CuboCrdtClientList<T> {
|
|
80
|
+
const storeKey = opts?.storeKey ?? `${entity}`
|
|
81
|
+
const filters = opts?.filters || {}
|
|
82
|
+
|
|
83
|
+
const subscribe_id = uuid()
|
|
84
|
+
|
|
85
|
+
// создаём хранилище
|
|
86
|
+
if (this.store[storeKey] === undefined) {
|
|
87
|
+
const state = reactive({
|
|
88
|
+
rows: [],
|
|
89
|
+
subscribed: false
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
this.store[storeKey] = {
|
|
93
|
+
state,
|
|
94
|
+
docs: new Map()
|
|
95
|
+
}
|
|
51
96
|
}
|
|
52
97
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
98
|
+
// подписка
|
|
99
|
+
const subscribe = (filters?: Record<string, any>) => {
|
|
100
|
+
if (this.store[storeKey]?.state.subscribed) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ставим сразу флаг, потому что не может быть ошибок (считаем так)
|
|
105
|
+
this.store[storeKey]!.state.subscribed = true
|
|
106
|
+
|
|
107
|
+
filters = filters !== undefined ? filters : opts?.filters
|
|
108
|
+
|
|
109
|
+
if (this.debug) {
|
|
110
|
+
console.log('[CRDT] subscribe', { entity, opts, filters })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// подписываемся на фронте
|
|
114
|
+
this.listeners.get(entity)?.set(subscribe_id, (ctx) => {
|
|
115
|
+
this.onIncomingUpdate(ctx, {
|
|
116
|
+
subscribe_id,
|
|
117
|
+
storeKey,
|
|
118
|
+
...(pick(opts || {}, ['onBeforeCreate', 'onBeforeUpdate', 'onBeforeDelete', 'onAfterCreate', 'onAfterUpdate', 'onAfterDelete']) ||
|
|
119
|
+
{})
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// подписываемся на бэке
|
|
124
|
+
this.ws.request({ method: CUBO_CRDT_EVENT.SUBSCRIBE, data: { subscribe_id, entity, filters } })
|
|
56
125
|
}
|
|
57
126
|
|
|
58
|
-
|
|
59
|
-
|
|
127
|
+
// отписка
|
|
128
|
+
const unsubscribe = (clear?: boolean) => {
|
|
129
|
+
if (this.debug) {
|
|
130
|
+
console.log('[CRDT] unsubscribe', { entity, opts, filters })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// отписываемся на бэке
|
|
134
|
+
this.ws.request({ method: CUBO_CRDT_EVENT.UNSUBSCRIBE, data: { subscribe_id } })
|
|
135
|
+
|
|
136
|
+
// отписываемся на фронте
|
|
137
|
+
this.listeners.get(entity)?.delete(subscribe_id)
|
|
60
138
|
|
|
61
|
-
|
|
62
|
-
|
|
139
|
+
// очищаем стор
|
|
140
|
+
if (clear !== false) {
|
|
141
|
+
this.store[storeKey] = undefined
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// апгрейд
|
|
146
|
+
const upgrade = (filters?: Record<string, any>) => {
|
|
147
|
+
// подписываемся на бэке
|
|
148
|
+
this.upgrade(subscribe_id, filters)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// авто-подписка
|
|
152
|
+
if (opts?.autoSubscribe !== false) {
|
|
153
|
+
subscribe()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
subscribe_id,
|
|
158
|
+
storeKey,
|
|
159
|
+
subscribe,
|
|
160
|
+
upgrade,
|
|
161
|
+
unsubscribe,
|
|
162
|
+
docs: computed(() => this.store[storeKey]?.docs),
|
|
163
|
+
subscribed: () => computed(() => this.store[storeKey]?.state.subscribed || false),
|
|
164
|
+
rows: () => computed(() => this.store[storeKey]?.state.rows as T[]),
|
|
165
|
+
rowsById: () => computed(() => keyBy(this.store[storeKey]?.state.rows as T[], 'id' as keyof T))
|
|
166
|
+
}
|
|
63
167
|
}
|
|
64
168
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
169
|
+
public useRow<K extends Extract<keyof M, string>, T = CuboCrdtKey<K, M>>(
|
|
170
|
+
entity: K | string,
|
|
171
|
+
id: number,
|
|
172
|
+
opts?: CuboCrdtClientUseOptions
|
|
173
|
+
): CuboCrdtClientRow<T> {
|
|
174
|
+
// console.log('store key', opts?.storeKey ?? `${entity}:${id}`)
|
|
175
|
+
|
|
176
|
+
const { subscribe_id, storeKey, docs, subscribe, upgrade, unsubscribe, subscribed } = this.useList(entity, {
|
|
177
|
+
...opts,
|
|
178
|
+
filters: opts?.filters ?? { id },
|
|
179
|
+
storeKey: opts?.storeKey ?? `${entity}:${id}`
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
subscribe_id,
|
|
184
|
+
storeKey,
|
|
185
|
+
doc: computed(() => docs.value?.get(id)),
|
|
186
|
+
subscribe,
|
|
187
|
+
upgrade,
|
|
188
|
+
unsubscribe,
|
|
189
|
+
subscribed,
|
|
190
|
+
row: () => {
|
|
191
|
+
return computed(() => {
|
|
192
|
+
return (this.store[storeKey]?.state.rows?.[0] ?? null) as T
|
|
193
|
+
})
|
|
194
|
+
}
|
|
68
195
|
}
|
|
196
|
+
}
|
|
69
197
|
|
|
70
|
-
|
|
71
|
-
|
|
198
|
+
public update<K extends string>(
|
|
199
|
+
entity: K,
|
|
200
|
+
entity_id: number,
|
|
201
|
+
dto: K extends Extract<keyof M, string> ? Partial<M[K]> : object,
|
|
202
|
+
opts?: CuboCrdtClientDocOrigin
|
|
203
|
+
) {
|
|
204
|
+
const cardRow = this.store[`${entity}:${entity_id}`]?.state.rows?.[0]
|
|
205
|
+
const listRow = this.store[entity]?.state?.rows.find((r: any) => r.id === entity_id)
|
|
206
|
+
const cardDoc = this.store[`${entity}:${entity_id}`]?.docs.get(entity_id)
|
|
207
|
+
const listDoc = this.store[entity]?.docs.get(entity_id)
|
|
208
|
+
|
|
209
|
+
// обновляем документы
|
|
210
|
+
for (const doc of [cardDoc, listDoc]) {
|
|
211
|
+
if (doc) {
|
|
212
|
+
const map = doc.getMap()
|
|
213
|
+
|
|
214
|
+
const row = (cardRow || listRow) as any
|
|
215
|
+
const toUpdate = Object.fromEntries(Object.entries(dto).filter(([key, value]) => row[key] !== value))
|
|
216
|
+
|
|
217
|
+
if (Object.keys(toUpdate).length) {
|
|
218
|
+
const origin: CuboCrdtClientDocOrigin = {
|
|
219
|
+
store: opts?.store ?? true,
|
|
220
|
+
expose: opts?.expose ?? 'other',
|
|
221
|
+
keys: opts?.keys ?? Object.keys(toUpdate),
|
|
222
|
+
react: true // отправляем на бэк
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
doc.transact(() => {
|
|
226
|
+
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
227
|
+
map.set(key, value)
|
|
228
|
+
})
|
|
229
|
+
}, origin)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// обновляем реактивку
|
|
235
|
+
for (const row of [cardRow, listRow]) {
|
|
236
|
+
if (row) {
|
|
237
|
+
const toUpdate = Object.fromEntries(Object.entries(dto).filter(([key, value]) => row[key as keyof typeof row] !== value))
|
|
238
|
+
|
|
239
|
+
if (Object.keys(toUpdate).length) {
|
|
240
|
+
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
241
|
+
row[key as keyof typeof row] = value
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
72
247
|
|
|
73
|
-
|
|
74
|
-
this.ws.request({ method:
|
|
248
|
+
public upgrade(subscribe_id: string, filters?: Record<string, any>) {
|
|
249
|
+
this.ws.request({ method: CUBO_CRDT_EVENT.UPGRADE, data: { subscribe_id, filters } })
|
|
75
250
|
}
|
|
76
251
|
|
|
77
|
-
async
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
252
|
+
private async onDocumentCreate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
253
|
+
let doc = this.store[opts.storeKey]?.docs.get(ctx.entity_id)
|
|
254
|
+
if (doc) {
|
|
255
|
+
console.warn('[CRDT] onDocumentCreate: doc already exists', { ctx, opts })
|
|
256
|
+
return
|
|
257
|
+
} else {
|
|
258
|
+
doc = new Doc()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const update = new Uint8Array(ctx.data as any)
|
|
262
|
+
|
|
263
|
+
if (opts?.onBeforeCreate) {
|
|
264
|
+
const result = await opts.onBeforeCreate(update, ctx, opts)
|
|
265
|
+
if (!result) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
applyUpdate(doc, update, { react: false })
|
|
271
|
+
|
|
272
|
+
// подписываемся на обновления документа
|
|
273
|
+
doc.on('update', (update, origin: CuboCrdtClientDocOrigin) => {
|
|
274
|
+
// react = false, если это апдейт с бэка
|
|
275
|
+
if (origin?.react !== false) {
|
|
276
|
+
const data: CuboCrdtServerDocumentIncomingAction = {
|
|
277
|
+
action: 'update',
|
|
278
|
+
entity: ctx.entity,
|
|
279
|
+
entity_id: ctx.entity_id,
|
|
280
|
+
data: Array.from(update),
|
|
281
|
+
origin: {
|
|
282
|
+
...pick(origin || {}, ['store', 'keys', 'expose']),
|
|
283
|
+
subscribe_id: opts?.subscribe_id
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.ws.request({
|
|
288
|
+
method: CUBO_CRDT_EVENT.EVENT,
|
|
289
|
+
data
|
|
290
|
+
})
|
|
291
|
+
}
|
|
88
292
|
})
|
|
89
293
|
|
|
90
|
-
|
|
294
|
+
// добавляем документ в хранилище документов
|
|
295
|
+
this.store[opts.storeKey]?.docs.set(ctx.entity_id, doc)
|
|
296
|
+
|
|
297
|
+
// добавляем документ в реактивное хранилище
|
|
298
|
+
const json: any = doc.getMap().toJSON()
|
|
299
|
+
const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === ctx.entity_id)
|
|
300
|
+
if (index !== undefined && index >= 0) {
|
|
301
|
+
if (this.debug) {
|
|
302
|
+
console.warn('[CRDT] onDocumentCreate - row already exists: "' + ctx.entity_id + '"', { ctx, opts })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.store[opts.storeKey]!.state.rows[index] = json
|
|
306
|
+
} else {
|
|
307
|
+
this.store[opts.storeKey]?.state.rows.push(json)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (opts?.onAfterCreate) {
|
|
311
|
+
await opts.onAfterCreate(doc, update, ctx, opts)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async onDocumentUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
316
|
+
const doc = this.store[opts.storeKey]?.docs.get(ctx.entity_id)
|
|
317
|
+
if (!doc) {
|
|
318
|
+
console.warn('[CRDT] onDocumentUpdate: doc not exists', { ctx, opts })
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const update = new Uint8Array(ctx.data as any)
|
|
323
|
+
|
|
324
|
+
if (opts?.onBeforeUpdate) {
|
|
325
|
+
const result = await opts.onBeforeUpdate(doc, update, ctx, opts)
|
|
326
|
+
if (!result) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// обновляем yjs-ный документ
|
|
332
|
+
applyUpdate(doc, update, { react: false })
|
|
333
|
+
|
|
334
|
+
// обновляем документ в реактивном хранилище
|
|
335
|
+
const json: any = doc.getMap().toJSON()
|
|
336
|
+
const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === ctx.entity_id)
|
|
337
|
+
if (index !== undefined && index >= 0) {
|
|
338
|
+
this.store[opts.storeKey]!.state.rows[index] = json
|
|
339
|
+
} else {
|
|
340
|
+
if (this.debug) {
|
|
341
|
+
console.warn('[CRDT] onDocumentUpdate - row not exists: "' + ctx.entity_id + '"', { ctx, opts })
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.store[opts.storeKey]!.state.rows.push(json)
|
|
345
|
+
}
|
|
91
346
|
|
|
92
|
-
|
|
347
|
+
if (opts?.onAfterUpdate) {
|
|
348
|
+
await opts.onAfterUpdate(doc, update, ctx, opts)
|
|
349
|
+
}
|
|
93
350
|
}
|
|
94
351
|
|
|
95
|
-
async
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
352
|
+
private async onDocumentDelete(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
353
|
+
const doc = this.store[opts.storeKey]?.docs.get(ctx.entity_id)
|
|
354
|
+
if (!doc) {
|
|
355
|
+
console.warn('[CRDT] onDocumentDelete: doc not exists', { ctx, opts })
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (opts?.onBeforeDelete) {
|
|
360
|
+
const result = await opts.onBeforeDelete(doc, ctx, opts)
|
|
361
|
+
if (!result) {
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// console.log('onDocumentDelete', ctx, opts)
|
|
367
|
+
|
|
368
|
+
doc.destroy()
|
|
369
|
+
this.store[opts.storeKey]?.docs.delete(ctx.entity_id)
|
|
370
|
+
|
|
371
|
+
const index = this.store[opts.storeKey]?.state.rows.findIndex((r: any) => r.id === ctx.entity_id)
|
|
372
|
+
if (index !== undefined && index >= 0) {
|
|
373
|
+
this.store[opts.storeKey]?.state.rows.splice(index, 1)
|
|
374
|
+
} else {
|
|
375
|
+
if (this.debug) {
|
|
376
|
+
console.log('[CRDT] row not found', index, ctx, opts)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (opts?.onAfterDelete) {
|
|
381
|
+
await opts.onAfterDelete(doc, ctx, opts)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private onIncomingUpdate(ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) {
|
|
386
|
+
if (this.debug) {
|
|
387
|
+
console.log('[CRDT] call event listener', ctx, opts)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!this.store[opts.storeKey]) {
|
|
391
|
+
console.warn('[CRDT] on incoming update, store key not found: "' + opts.storeKey + '"')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
switch (ctx.action) {
|
|
395
|
+
case 'create':
|
|
396
|
+
this.onDocumentCreate(ctx, opts)
|
|
397
|
+
break
|
|
398
|
+
case 'update':
|
|
399
|
+
case 'upsert':
|
|
400
|
+
this.onDocumentUpdate(ctx, opts)
|
|
401
|
+
break
|
|
402
|
+
case 'delete':
|
|
403
|
+
this.onDocumentDelete(ctx, opts)
|
|
404
|
+
break
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private get ws() {
|
|
409
|
+
return this.opts.ws
|
|
410
|
+
}
|
|
103
411
|
|
|
104
|
-
|
|
412
|
+
private get debug() {
|
|
413
|
+
return this.opts.debug
|
|
105
414
|
}
|
|
106
415
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CuboCrdtExposeStrategy } from '../../types'
|
|
2
|
+
|
|
3
|
+
import { CuboCrdtClientBaseOptions } from './utils'
|
|
4
|
+
|
|
5
|
+
export type CuboCrdtClientDocOrigin = {
|
|
6
|
+
// сохранять ли на бэке в дебаунсе
|
|
7
|
+
store?: boolean
|
|
8
|
+
|
|
9
|
+
// реагировать ли в onUpdate (на фронте)
|
|
10
|
+
react?: boolean
|
|
11
|
+
|
|
12
|
+
// обновлённые ключи
|
|
13
|
+
keys?: string[]
|
|
14
|
+
|
|
15
|
+
// куда раскатывать обновления - всем или всем кроме себя
|
|
16
|
+
expose?: CuboCrdtExposeStrategy
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CuboCrdtClientDocUpdateOptions = { subscribe_id: string; storeKey: string } & Pick<
|
|
20
|
+
CuboCrdtClientBaseOptions,
|
|
21
|
+
'onBeforeCreate' | 'onAfterCreate' | 'onBeforeUpdate' | 'onAfterUpdate' | 'onBeforeDelete' | 'onAfterDelete'
|
|
22
|
+
>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type WsClient } from '@cuboapp/ws'
|
|
2
|
+
import { ComputedRef } from 'vue'
|
|
3
|
+
import { type Doc } from 'yjs'
|
|
4
|
+
|
|
5
|
+
import { CuboCrdtAction } from '../../types'
|
|
6
|
+
|
|
7
|
+
import { CuboCrdtClientBaseOptions } from './utils'
|
|
8
|
+
|
|
9
|
+
export * from './document'
|
|
10
|
+
export * from './store'
|
|
11
|
+
export * from './utils'
|
|
12
|
+
|
|
13
|
+
export type CuboCrdtClientOptions<M> = {
|
|
14
|
+
ws: WsClient
|
|
15
|
+
entities: Extract<keyof M, string>[]
|
|
16
|
+
debug?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CuboCrdtClientUseOptions = CuboCrdtClientBaseOptions & {
|
|
20
|
+
storeKey?: string
|
|
21
|
+
filters?: Record<string, any>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CuboCrdtClientSubscribeEvent = {
|
|
25
|
+
action: CuboCrdtAction
|
|
26
|
+
entity: string
|
|
27
|
+
entity_id: number
|
|
28
|
+
subscribe_id?: string
|
|
29
|
+
data?: number[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CuboCrdtClientList<T> = {
|
|
33
|
+
subscribe_id: string
|
|
34
|
+
storeKey: string
|
|
35
|
+
docs: ComputedRef<Map<number, Doc> | undefined>
|
|
36
|
+
subscribe: (filters?: any) => void
|
|
37
|
+
upgrade: (filters?: any) => void
|
|
38
|
+
unsubscribe: (clear?: boolean) => void
|
|
39
|
+
subscribed: () => ComputedRef<boolean>
|
|
40
|
+
rows: () => ComputedRef<T[]>
|
|
41
|
+
rowsById: () => ComputedRef<Record<string, T>>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type CuboCrdtClientRow<T> = {
|
|
45
|
+
subscribe_id: string
|
|
46
|
+
storeKey: string
|
|
47
|
+
doc: ComputedRef<Doc | undefined>
|
|
48
|
+
subscribe: () => void
|
|
49
|
+
upgrade: (filters?: any) => void
|
|
50
|
+
unsubscribe: (clear?: boolean) => void
|
|
51
|
+
subscribed: () => ComputedRef<boolean>
|
|
52
|
+
row: () => ComputedRef<T>
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Reactive } from 'vue'
|
|
2
|
+
import { Doc } from 'yjs'
|
|
3
|
+
|
|
4
|
+
import { CuboCrdtKey } from '../../types'
|
|
5
|
+
|
|
6
|
+
export type CuboCrdtClientStoreItem<T> = {
|
|
7
|
+
state: Reactive<{
|
|
8
|
+
rows: T[]
|
|
9
|
+
subscribed: boolean
|
|
10
|
+
}>
|
|
11
|
+
docs: Map<number, Doc>
|
|
12
|
+
}
|
|
13
|
+
export type CuboCrdtClientStore<M> = Partial<{
|
|
14
|
+
[K in string]: CuboCrdtClientStoreItem<CuboCrdtKey<K, M>>
|
|
15
|
+
}>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Doc } from 'yjs'
|
|
2
|
+
import { CuboCrdtClientSubscribeEvent } from '.'
|
|
3
|
+
import { CuboCrdtClientDocUpdateOptions } from './document'
|
|
4
|
+
|
|
5
|
+
export type CuboCrdtClientBaseOptions = {
|
|
6
|
+
autoSubscribe?: boolean
|
|
7
|
+
|
|
8
|
+
onBeforeCreate?: (
|
|
9
|
+
update: Uint8Array,
|
|
10
|
+
ctx: CuboCrdtClientSubscribeEvent,
|
|
11
|
+
opts: CuboCrdtClientDocUpdateOptions
|
|
12
|
+
) => Promise<boolean> | boolean
|
|
13
|
+
onAfterCreate?: (
|
|
14
|
+
doc: Doc,
|
|
15
|
+
update: Uint8Array,
|
|
16
|
+
ctx: CuboCrdtClientSubscribeEvent,
|
|
17
|
+
opts: CuboCrdtClientDocUpdateOptions
|
|
18
|
+
) => Promise<void> | void
|
|
19
|
+
|
|
20
|
+
onBeforeUpdate?: (
|
|
21
|
+
doc: Doc,
|
|
22
|
+
update: Uint8Array,
|
|
23
|
+
ctx: CuboCrdtClientSubscribeEvent,
|
|
24
|
+
opts: CuboCrdtClientDocUpdateOptions
|
|
25
|
+
) => Promise<boolean> | boolean
|
|
26
|
+
onAfterUpdate?: (
|
|
27
|
+
doc: Doc,
|
|
28
|
+
update: Uint8Array,
|
|
29
|
+
ctx: CuboCrdtClientSubscribeEvent,
|
|
30
|
+
opts: CuboCrdtClientDocUpdateOptions
|
|
31
|
+
) => Promise<void> | void
|
|
32
|
+
|
|
33
|
+
onBeforeDelete?: (doc: Doc, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<boolean> | boolean
|
|
34
|
+
onAfterDelete?: (doc: Doc, ctx: CuboCrdtClientSubscribeEvent, opts: CuboCrdtClientDocUpdateOptions) => Promise<void> | void
|
|
35
|
+
}
|