@cuboapp/crdt 1.0.5 → 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.
- package/package.json +8 -4
- package/src/client/index.ts +350 -72
- package/src/client/old.ts +249 -0
- package/src/client/types/document.ts +22 -0
- package/src/client/types/index.ts +51 -0
- package/src/client/types/store.ts +15 -0
- package/src/client/types/utils.ts +35 -0
- package/src/constants/index.ts +5 -0
- package/src/index.ts +1 -129
- package/src/server/document/index.ts +91 -0
- package/src/server/index.ts +449 -0
- package/src/server/old/document/index.ts +107 -0
- package/src/server/old/index.ts +49 -0
- package/src/server/types/document.ts +54 -0
- package/src/server/types/index.ts +22 -0
- package/src/server/types/subscribe.ts +15 -0
- package/src/types/index.ts +3 -9
- package/src/utils/index.ts +65 -3
- package/src/yjs/index.ts +0 -118
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { uuid } from '@cuboapp/utils'
|
|
2
|
+
import { WsClient } from '@cuboapp/ws'
|
|
3
|
+
import { computed, ComputedRef, Reactive, reactive } from 'vue'
|
|
4
|
+
import { applyUpdate, Doc } from 'yjs'
|
|
5
|
+
|
|
6
|
+
export type CuboCrdtClientStoreElement<M, K> = {
|
|
7
|
+
rows: Reactive<Map<number, K extends keyof M ? M[K] : unknown>>
|
|
8
|
+
docs: Map<number, Doc>
|
|
9
|
+
flags: Reactive<{
|
|
10
|
+
inited: boolean
|
|
11
|
+
}>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CuboCrdtClientSubscribeOptions<S, F> = {
|
|
15
|
+
entity_id?: number
|
|
16
|
+
storeKey?: S
|
|
17
|
+
filters?: F
|
|
18
|
+
autoInit?: boolean
|
|
19
|
+
onUpdate?: () => void
|
|
20
|
+
onInit?: () => void
|
|
21
|
+
debug?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CuboCrdtClientSubscribe<M = {}, K = {}, S = {}, F = {}> = {
|
|
25
|
+
id: string
|
|
26
|
+
storeKey: S
|
|
27
|
+
store: CuboCrdtClientStoreElement<M, K>
|
|
28
|
+
init: (filters?: F) => void
|
|
29
|
+
destroy: (opts?: { clear?: boolean }) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type CuboCrdtClientOptions = {
|
|
33
|
+
ws: WsClient
|
|
34
|
+
debug?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class CuboCrdtClient<M> {
|
|
38
|
+
constructor(private opts: CuboCrdtClientOptions) {}
|
|
39
|
+
|
|
40
|
+
public store: Partial<{ [K in Extract<keyof M, string>]: CuboCrdtClientStoreElement<M, K> }> = {}
|
|
41
|
+
|
|
42
|
+
private get ws() {
|
|
43
|
+
return this.opts.ws
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private getFilters<F>(filters: F) {
|
|
47
|
+
if (Array.isArray(filters)) {
|
|
48
|
+
return filters.map(this.getFilters).filter(Boolean)
|
|
49
|
+
} else if (typeof filters === 'object' && filters !== null) {
|
|
50
|
+
const newObj: any = {}
|
|
51
|
+
|
|
52
|
+
for (const key in filters) {
|
|
53
|
+
const value = this.getFilters(filters[key])
|
|
54
|
+
|
|
55
|
+
if (value !== undefined && value !== null && value !== '' && !(typeof value === 'object' && Object.keys(value).length === 0)) {
|
|
56
|
+
newObj[key] = value
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return newObj
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return filters
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
subscribe<K extends Extract<keyof M, string>, S extends string, F extends Record<string, any>>(
|
|
67
|
+
entity: K,
|
|
68
|
+
opts?: CuboCrdtClientSubscribeOptions<S, F>
|
|
69
|
+
): CuboCrdtClientSubscribe<M, K, S, F> {
|
|
70
|
+
const subscribe_id = uuid()
|
|
71
|
+
const storeKey = opts?.storeKey ?? (entity as any)
|
|
72
|
+
|
|
73
|
+
if (opts?.debug) {
|
|
74
|
+
console.trace('subscribe tasks', subscribe_id, opts)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (this.store[storeKey] === undefined) {
|
|
78
|
+
this.store[storeKey] = {
|
|
79
|
+
flags: reactive({
|
|
80
|
+
inited: false
|
|
81
|
+
}),
|
|
82
|
+
rows: reactive(new Map()),
|
|
83
|
+
docs: new Map()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onUpdateDocument = (id: number, data: Uint8Array, fireOnUpdate?: boolean) => {
|
|
88
|
+
let item: any
|
|
89
|
+
|
|
90
|
+
// обновляем документ
|
|
91
|
+
|
|
92
|
+
let exDoc = this.store[storeKey].docs.get(id)
|
|
93
|
+
if (!exDoc) {
|
|
94
|
+
const doc = new Doc()
|
|
95
|
+
applyUpdate(doc, data)
|
|
96
|
+
|
|
97
|
+
doc.on('update', (update, origin) => {
|
|
98
|
+
if (!origin?.no_push) {
|
|
99
|
+
this.ws.request({
|
|
100
|
+
method: 'crdt:update',
|
|
101
|
+
data: {
|
|
102
|
+
subscribe_id,
|
|
103
|
+
name: `${entity}:${id}`,
|
|
104
|
+
update: Array.from(update),
|
|
105
|
+
origin
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
this.store[storeKey].docs.set(id, doc)
|
|
112
|
+
exDoc = doc
|
|
113
|
+
} else {
|
|
114
|
+
applyUpdate(exDoc, data, { no_push: true })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
item = exDoc.getMap().toJSON()
|
|
118
|
+
|
|
119
|
+
// обновляем строку
|
|
120
|
+
this.store[storeKey].rows.set(id, item as any)
|
|
121
|
+
|
|
122
|
+
if (opts?.debug) {
|
|
123
|
+
console.log('fire onUpdate', { fireOnUpdate })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (fireOnUpdate !== false) {
|
|
127
|
+
opts?.onUpdate?.()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const init = (filters?: F) => {
|
|
132
|
+
filters = filters !== undefined ? filters : opts?.filters
|
|
133
|
+
|
|
134
|
+
if (this.store[storeKey].flags.inited) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// подписываемся на обновления
|
|
139
|
+
this.ws.registerHandler('crdt:action:' + subscribe_id, (ctx) => {
|
|
140
|
+
const { action, entity_id, data } = ctx.message.data as any
|
|
141
|
+
|
|
142
|
+
switch (action) {
|
|
143
|
+
case 'update':
|
|
144
|
+
onUpdateDocument(entity_id, new Uint8Array(data), true)
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
this.ws
|
|
150
|
+
.request(
|
|
151
|
+
{
|
|
152
|
+
method: 'crdt:subscribe',
|
|
153
|
+
data: this.getFilters({
|
|
154
|
+
id: subscribe_id,
|
|
155
|
+
entity,
|
|
156
|
+
entity_id: opts?.entity_id,
|
|
157
|
+
filters
|
|
158
|
+
})
|
|
159
|
+
},
|
|
160
|
+
true
|
|
161
|
+
)
|
|
162
|
+
.then((data: any) => {
|
|
163
|
+
if (opts?.entity_id) {
|
|
164
|
+
onUpdateDocument(data.id, new Uint8Array(data.row), false)
|
|
165
|
+
} else {
|
|
166
|
+
data.map(({ id, row }: { id: number; row: number[] }) => onUpdateDocument(id, new Uint8Array(row), false))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.store[storeKey].flags.inited = true
|
|
170
|
+
opts?.onInit?.()
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const destroy = ({ clear }: { clear?: boolean } = {}) => {
|
|
175
|
+
if (!this.store[storeKey].flags.inited) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.ws.request(
|
|
180
|
+
{
|
|
181
|
+
method: 'crdt:unsubscribe',
|
|
182
|
+
data: this.getFilters({
|
|
183
|
+
id: subscribe_id,
|
|
184
|
+
entity,
|
|
185
|
+
entity_id: opts?.entity_id,
|
|
186
|
+
filters: opts?.filters
|
|
187
|
+
})
|
|
188
|
+
},
|
|
189
|
+
true
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
this.ws.deleteHandler('crdt:update:' + subscribe_id)
|
|
193
|
+
|
|
194
|
+
if (clear !== false) {
|
|
195
|
+
this.store[storeKey] = undefined
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (opts?.autoInit !== false) {
|
|
200
|
+
init()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
id: subscribe_id,
|
|
205
|
+
storeKey,
|
|
206
|
+
store: this.store[storeKey] as any,
|
|
207
|
+
init,
|
|
208
|
+
destroy
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
useList<K extends string, T = K extends keyof M ? M[K] : unknown>(key: K): ComputedRef<T[]> {
|
|
213
|
+
return computed(() => {
|
|
214
|
+
return Array.from(this.store[key as any].rows || []).map((m) => m[1] as T)
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
change<K extends string = Extract<keyof M, string>, T = K extends keyof M ? M[K] : unknown>(
|
|
219
|
+
entity: K,
|
|
220
|
+
entity_id: number,
|
|
221
|
+
dto: Partial<T> & Record<string, any>,
|
|
222
|
+
opts?: { strategy?: 'other' | 'all' }
|
|
223
|
+
) {
|
|
224
|
+
const row = this.store[entity as any].rows.get(entity_id)
|
|
225
|
+
const doc = this.store[entity as any].docs.get(entity_id)
|
|
226
|
+
|
|
227
|
+
if (doc) {
|
|
228
|
+
const map = doc.getMap()
|
|
229
|
+
|
|
230
|
+
const toUpdate = Object.fromEntries(Object.entries(dto).filter(([key, value]) => row[key] !== value))
|
|
231
|
+
|
|
232
|
+
if (Object.keys(toUpdate).length) {
|
|
233
|
+
doc.transact(
|
|
234
|
+
() => {
|
|
235
|
+
Object.entries(toUpdate).forEach(([key, value]) => {
|
|
236
|
+
map.set(key, value)
|
|
237
|
+
row[key] = value
|
|
238
|
+
})
|
|
239
|
+
},
|
|
240
|
+
{ with_save: true, strategy: opts?.strategy ?? 'other', keys: Object.keys(toUpdate) }
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
useRow<K extends Extract<keyof M, string>, T = K extends keyof M ? M[K] : unknown>(entity: K, entity_id: number): ComputedRef<T> {
|
|
247
|
+
return computed(() => this.store[entity].rows.get(entity_id)) as ComputedRef<T>
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -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,51 @@
|
|
|
1
|
+
import { WsClient } from '@cuboapp/ws'
|
|
2
|
+
import { ComputedRef } from 'vue'
|
|
3
|
+
import { 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
|
+
unsubscribe: (clear?: boolean) => void
|
|
38
|
+
subscribed: () => ComputedRef<boolean>
|
|
39
|
+
rows: () => ComputedRef<T[]>
|
|
40
|
+
rowsById: () => ComputedRef<Record<string, T>>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CuboCrdtClientRow<T> = {
|
|
44
|
+
subscribe_id: string
|
|
45
|
+
storeKey: string
|
|
46
|
+
doc: ComputedRef<Doc | undefined>
|
|
47
|
+
subscribe: () => void
|
|
48
|
+
unsubscribe: (clear?: boolean) => void
|
|
49
|
+
subscribed: () => ComputedRef<boolean>
|
|
50
|
+
row: () => ComputedRef<T>
|
|
51
|
+
}
|
|
@@ -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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,132 +1,4 @@
|
|
|
1
|
-
import { type CuboApiEntitiesMap } from '@cuboapp/types'
|
|
2
|
-
import { EventEmitter } from '@cuboapp/utils'
|
|
3
|
-
|
|
4
|
-
import { CuboCrdtOptions } from './types'
|
|
5
|
-
import { CuboCrdtDocument } from './yjs'
|
|
6
|
-
|
|
7
1
|
export * from './client'
|
|
2
|
+
export * from './server'
|
|
8
3
|
export * from './types'
|
|
9
4
|
export * from './utils'
|
|
10
|
-
export * from './yjs'
|
|
11
|
-
|
|
12
|
-
export const CUBO_CRDT_EVENT = {
|
|
13
|
-
DOCUMENT_UPDATED: 'document:updated',
|
|
14
|
-
DOCUMENT_DELETED: 'document:deleted',
|
|
15
|
-
DOCUMENT_CREATED: 'document:created'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class CuboCrdt<
|
|
19
|
-
T extends CuboApiEntitiesMap<T>,
|
|
20
|
-
A = {},
|
|
21
|
-
K extends string = Extract<keyof T, string>,
|
|
22
|
-
ID extends string = `${K}:${number | string}`
|
|
23
|
-
> {
|
|
24
|
-
public documents = new Map<ID, CuboCrdtDocument>()
|
|
25
|
-
private loading = new Map<ID, Promise<CuboCrdtDocument>>()
|
|
26
|
-
|
|
27
|
-
public ee = new EventEmitter()
|
|
28
|
-
|
|
29
|
-
constructor(private options: CuboCrdtOptions<A>) {}
|
|
30
|
-
|
|
31
|
-
public async start() {
|
|
32
|
-
// setInterval(() => {
|
|
33
|
-
// console.log('documents', this.documents.size)
|
|
34
|
-
// }, 1000)
|
|
35
|
-
// const doc = new Doc()
|
|
36
|
-
// const map = doc.getMap('store')
|
|
37
|
-
// doc.on('update', (upd) => {
|
|
38
|
-
// const map1 = doc.getMap('store')
|
|
39
|
-
// console.log('new state', map1.toJSON())
|
|
40
|
-
// })
|
|
41
|
-
// doc.transact(() => {
|
|
42
|
-
// map.set('key', 'value')
|
|
43
|
-
// })
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public async stop() {}
|
|
47
|
-
|
|
48
|
-
public async getDocument(name: ID, autoCreate = true) {
|
|
49
|
-
// существующий документ
|
|
50
|
-
if (!this.documents.has(name)) {
|
|
51
|
-
if (!this.loading.has(name) && autoCreate) {
|
|
52
|
-
if (this.options.debug) {
|
|
53
|
-
console.log('CRDT create document load start', name)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
this.loading.set(
|
|
57
|
-
name,
|
|
58
|
-
new Promise(async (resolve) => {
|
|
59
|
-
const document = new CuboCrdtDocument({
|
|
60
|
-
name,
|
|
61
|
-
autoRemove: true,
|
|
62
|
-
debug: this.options.debug,
|
|
63
|
-
initialState: () => {
|
|
64
|
-
return this.options.beforeLoadDocument?.(name)
|
|
65
|
-
},
|
|
66
|
-
onUpdate: ({ data, document }) => {
|
|
67
|
-
this.ee.emit(CUBO_CRDT_EVENT.DOCUMENT_UPDATED, { name, data, document })
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
await document.init()
|
|
72
|
-
|
|
73
|
-
resolve(document)
|
|
74
|
-
})
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const document = await this.loading.get(name)
|
|
79
|
-
|
|
80
|
-
this.loading.delete(name)
|
|
81
|
-
|
|
82
|
-
if (document) {
|
|
83
|
-
this.documents.set(name, document)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (this.options.debug) {
|
|
88
|
-
console.log('CRDT create document load completed', name, this.documents.size)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return this.documents.get(name)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
public async removeDocument(name: ID) {
|
|
95
|
-
if (this.options.debug) {
|
|
96
|
-
console.log('CRDT remove document', name)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (this.loading.has(name)) {
|
|
100
|
-
try {
|
|
101
|
-
Promise.reject(this.loading.get(name))
|
|
102
|
-
this.loading.delete(name)
|
|
103
|
-
} catch {}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (this.documents.has(name)) {
|
|
107
|
-
this.documents.delete(name)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
public async updateDocument(name: ID, dto: object) {
|
|
112
|
-
const document = await this.getDocument(name)
|
|
113
|
-
|
|
114
|
-
if (document) {
|
|
115
|
-
await document.write(dto)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
await this.checkDocumentUnload(name)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
public async checkDocumentUnload(name: ID) {
|
|
122
|
-
const doc = this.documents.get(name)
|
|
123
|
-
|
|
124
|
-
if (doc?.opts?.autoRemove && doc.clients.size === 0) {
|
|
125
|
-
if (this.options.debug) {
|
|
126
|
-
console.log('CRDT destroy document', name)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
this.documents.delete(name)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { debounce, pick } from '@cuboapp/utils'
|
|
2
|
+
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'
|
|
3
|
+
|
|
4
|
+
import { CuboCrdtExposeStrategy } from '../../types'
|
|
5
|
+
|
|
6
|
+
import { CuboCrdtServerDocumentOptions, CuboCrdtServerDocumentOrigin } from '../types'
|
|
7
|
+
|
|
8
|
+
export class CuboCrdtServerDocument {
|
|
9
|
+
private ydoc: Doc
|
|
10
|
+
|
|
11
|
+
private firstUpdate = true
|
|
12
|
+
|
|
13
|
+
constructor(public opts: CuboCrdtServerDocumentOptions) {
|
|
14
|
+
this.ydoc = new Doc({
|
|
15
|
+
...(this.opts.yjsOptions || {}),
|
|
16
|
+
autoLoad: false
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
this.ydoc.on('update', (data, origin: CuboCrdtServerDocumentOrigin) => {
|
|
20
|
+
if (this.firstUpdate) {
|
|
21
|
+
this.firstUpdate = true
|
|
22
|
+
this.opts?.onCreate?.(data, origin)
|
|
23
|
+
} else {
|
|
24
|
+
this.opts?.onUpdate?.(data, origin)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (origin?.store) {
|
|
28
|
+
const row = this.getJson()
|
|
29
|
+
const body = pick(row, origin.keys ?? Object.keys(row))
|
|
30
|
+
|
|
31
|
+
if (Object.keys(body)) {
|
|
32
|
+
this.debounceStore(body, origin)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public get name() {
|
|
39
|
+
return this.opts.name
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public get stateAsUpdate() {
|
|
43
|
+
return encodeStateAsUpdate(this.ydoc)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
applyUpdate(update: Uint8Array, origin?: CuboCrdtServerDocumentOrigin) {
|
|
47
|
+
return applyUpdate(this.ydoc, update, origin)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getMap() {
|
|
51
|
+
return this.ydoc.getMap()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getJson() {
|
|
55
|
+
return this.getMap().toJSON()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async write(dto: object, origin?: CuboCrdtServerDocumentOrigin) {
|
|
59
|
+
const map = this.getMap()
|
|
60
|
+
|
|
61
|
+
this.ydoc.transact(() => {
|
|
62
|
+
Object.entries(dto).forEach(([key, value]) => {
|
|
63
|
+
map.set(key, value)
|
|
64
|
+
})
|
|
65
|
+
}, origin)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get id() {
|
|
69
|
+
return this.ydoc.guid
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
destroy() {
|
|
73
|
+
this.ydoc.destroy()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private _inited = false
|
|
77
|
+
public init(state?: object, expose: CuboCrdtExposeStrategy = 'all') {
|
|
78
|
+
if (this._inited) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this._inited = true
|
|
83
|
+
|
|
84
|
+
this.write(state ?? {}, { expose })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public store(body: object, origin: CuboCrdtServerDocumentOrigin) {
|
|
88
|
+
this.opts?.onStore?.(body, origin)
|
|
89
|
+
}
|
|
90
|
+
public debounceStore = debounce(this.store.bind(this), 300)
|
|
91
|
+
}
|