@cuboapp/crdt 1.0.0 → 1.0.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuboapp/crdt",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CRDT for CUBO",
5
5
  "main": "src/index.ts",
6
6
  "repository": "git@github.com:cuboapp/crdt.git",
@@ -11,12 +11,9 @@
11
11
  "access": "public"
12
12
  },
13
13
  "dependencies": {
14
- "@cuboapp/constants": "^2.0.6",
15
- "@cuboapp/database": "^1.0.3",
16
14
  "@cuboapp/types": "^2.0.11",
17
15
  "@cuboapp/utils": "1.0.9",
18
- "@cuboapp/ws": "../ws",
19
- "pg": "^8.16.3",
16
+ "@cuboapp/ws": "1.0.4",
20
17
  "ws": "^8.19.0",
21
18
  "yjs": "^13.6.29"
22
19
  },
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from '@cuboapp/utils'
2
2
  import { createWsClient, WsClient } from '@cuboapp/ws'
3
+ import { applyUpdate, Doc } from 'yjs'
3
4
 
4
5
  export type CuboCrdtClientOptions = {
5
6
  host?: string
@@ -42,25 +43,37 @@ export class CuboCrdtClient {
42
43
  }
43
44
  }
44
45
 
45
- async subscribe(event: string, cb: any, filters?: any) {
46
+ async subscribe({ entity, id, filters }: { entity: string; id?: number; filters?: any }, cb: any) {
46
47
  if (this.opts.debug) {
47
- console.log('subscribe', event)
48
+ console.log('subscribeEntity', { entity, id })
48
49
  }
49
50
 
51
+ filters = filters || {}
52
+ if (id) {
53
+ filters.id = id
54
+ }
55
+
56
+ const event = `crdt:${entity}`
57
+ // const event = id ? `crdt:${entity}:${id}` : `crdt:${entity}`
58
+
50
59
  this.ee.addListener(event, cb)
51
60
  this.ws.request({ method: 'crdt:subscribe', data: { event, filters } })
52
61
  }
53
62
 
54
- async unsubscribe(event: string, cb: any) {
63
+ async unsubscribe({ entity, id, filters }: { entity: string; id?: number; filters?: any }, cb: any) {
55
64
  if (this.opts.debug) {
56
- console.log('unsubscribe', event)
65
+ console.log('subscribeEntity', { entity, id })
57
66
  }
67
+
68
+ const event = `crdt:${entity}`
69
+ // const event = id ? `crdt:${entity}:${id}` : `crdt:${entity}`
70
+
58
71
  this.ee.removeListener(event, cb)
59
- this.ws.request({ method: 'crdt:unsubscribe', data: { event } })
72
+ this.ws.request({ method: 'crdt:unsubscribe', data: { event, filters } })
60
73
  }
61
74
 
62
- async openDocument<T>(entity: string, id: number): Promise<T> {
63
- const state = await this.ws.request(
75
+ async openDocument(entity: string, id: number, doc: Doc): Promise<void> {
76
+ const state: any = await this.ws.request(
64
77
  {
65
78
  method: 'crdt:open',
66
79
  data: { name: `${entity}:${id}` }
@@ -68,7 +81,13 @@ export class CuboCrdtClient {
68
81
  { wait: true }
69
82
  )
70
83
 
71
- return state as T
84
+ this.ee.addListener(`crdt:update:${entity}:${id}`, (ctx) => {
85
+ applyUpdate(doc, new Uint8Array(ctx.data))
86
+ })
87
+
88
+ this.ws.request({ method: 'crdt:subscribe', data: { crdt: { entity, id } } })
89
+
90
+ applyUpdate(doc, new Uint8Array(state))
72
91
  }
73
92
 
74
93
  async closeDocument(entity: string, id: number) {
@@ -79,5 +98,7 @@ export class CuboCrdtClient {
79
98
  },
80
99
  { wait: true }
81
100
  )
101
+
102
+ this.ee.removeAllListeners(`crdt:document:${entity}:${id}:update`)
82
103
  }
83
104
  }
package/src/index.ts CHANGED
@@ -21,29 +21,47 @@ export class CuboCrdt<
21
21
  K extends string = Extract<keyof T, string>,
22
22
  ID extends string = `${K}:${number | string}`
23
23
  > {
24
- private documents = new Map<ID, CuboCrdtDocument>()
24
+ public documents = new Map<ID, CuboCrdtDocument>()
25
25
  private loading = new Map<ID, Promise<CuboCrdtDocument>>()
26
26
 
27
27
  public ee = new EventEmitter()
28
28
 
29
29
  constructor(private options: CuboCrdtOptions<A>) {}
30
30
 
31
- public async start() {}
31
+ public async start() {
32
+ setInterval(() => {
33
+ // console.log('documents', this.documents.size)
34
+ }, 1000)
35
+
36
+ // const doc = new Doc()
37
+ // const map = doc.getMap('store')
38
+
39
+ // doc.on('update', (upd) => {
40
+ // const map1 = doc.getMap('store')
41
+
42
+ // console.log('new state', map1.toJSON())
43
+ // })
44
+
45
+ // doc.transact(() => {
46
+ // map.set('key', 'value')
47
+ // })
48
+ }
32
49
 
33
50
  public async stop() {}
34
51
 
35
- public async getDocument(name: ID) {
52
+ public async getDocument(name: ID, autoCreate = true) {
36
53
  // существующий документ
37
54
  if (!this.documents.has(name)) {
38
- if (this.options.debug) {
39
- console.log('CRDT create document', name)
40
- }
55
+ if (!this.loading.has(name) && autoCreate) {
56
+ if (this.options.debug) {
57
+ console.log('CRDT create document load start', name)
58
+ }
41
59
 
42
- if (!this.loading.has(name)) {
43
60
  this.loading.set(
44
61
  name,
45
62
  new Promise(async (resolve) => {
46
63
  const document = new CuboCrdtDocument({
64
+ name,
47
65
  autoRemove: true,
48
66
  debug: this.options.debug,
49
67
  initialState: () => {
@@ -61,12 +79,36 @@ export class CuboCrdt<
61
79
  )
62
80
  }
63
81
 
64
- this.documents.set(name, await this.loading.get(name)!)
82
+ const document = await this.loading.get(name)
83
+
84
+ this.loading.delete(name)
85
+ this.documents.set(name, document)
86
+ }
87
+
88
+ if (this.options.debug) {
89
+ console.log('CRDT create document load completed', name, this.documents.size)
65
90
  }
66
91
 
67
92
  return this.documents.get(name)
68
93
  }
69
94
 
95
+ public async removeDocument(name: ID) {
96
+ if (this.options.debug) {
97
+ console.log('CRDT remove document', name)
98
+ }
99
+
100
+ if (this.loading.has(name)) {
101
+ try {
102
+ Promise.reject(this.loading.get(name))
103
+ this.loading.delete(name)
104
+ } catch {}
105
+ }
106
+
107
+ if (this.documents.has(name)) {
108
+ this.documents.delete(name)
109
+ }
110
+ }
111
+
70
112
  public async updateDocument(name: ID, dto: object) {
71
113
  const document = await this.getDocument(name)
72
114
 
package/src/yjs/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type WsServerSocket } from '@cuboapp/ws'
2
- import { Doc } from 'yjs'
2
+ import { Doc, encodeStateAsUpdate } from 'yjs'
3
3
 
4
4
  export type CuboCrdtDocumentHooks = {
5
5
  onBeforeDestroy?: () => Promise<void> | void
@@ -7,6 +7,7 @@ export type CuboCrdtDocumentHooks = {
7
7
  }
8
8
 
9
9
  export type CuboCrdtDocumentOptions = CuboCrdtDocumentHooks & {
10
+ name: string
10
11
  autoRemove?: boolean
11
12
  debug?: boolean
12
13
  initial?: any
@@ -27,8 +28,7 @@ export class CuboCrdtDocument {
27
28
  private ydoc: Doc
28
29
  public clients = new Set<WsServerSocket>()
29
30
 
30
- constructor(public opts?: CuboCrdtDocumentOptions) {
31
- opts = opts || {}
31
+ constructor(public opts: CuboCrdtDocumentOptions) {
32
32
  opts.autoRemove = opts?.autoRemove ?? true
33
33
  }
34
34
 
@@ -38,36 +38,48 @@ export class CuboCrdtDocument {
38
38
  }
39
39
  }
40
40
 
41
+ get stateAsUpdate() {
42
+ return encodeStateAsUpdate(this.ydoc)
43
+ }
44
+
41
45
  async init() {
46
+ console.log('init doc', this.opts.name)
47
+
42
48
  this.ydoc = new Doc({
43
49
  ...(this.opts?.yjsOptions || {}),
44
50
  autoLoad: false,
45
51
  shouldLoad: false
46
52
  })
47
53
 
48
- const state = this.opts?.initialState ? await this.opts?.initialState() : {}
54
+ const state = this.opts?.initialState ? (await this.opts?.initialState()) || {} : {}
55
+
56
+ // console.log('initial state', state)
49
57
 
50
58
  return new Promise<void>((resolve) => {
51
- this.ydoc.once('updateV2', () => {
52
- this.ydoc.on('updateV2', (data) => {
59
+ this.ydoc.once('update', () => {
60
+ this.ydoc.on('update', (data) => {
61
+ // console.log('document updated', this.opts.name, this.getMap().toJSON())
62
+
53
63
  this.opts?.onUpdate?.({ data, document: this })
54
64
  })
55
65
 
56
66
  resolve()
57
67
  })
58
68
 
59
- this.ydoc.transact((t) => {
60
- const map = t.doc.getMap()
69
+ const map = this.getMap()
61
70
 
62
- Object.entries(state).forEach(([key, value]) => {
63
- map.set(key, value)
64
- })
71
+ this.ydoc.transact(() => {
72
+ if (state) {
73
+ Object.entries(state).forEach(([key, value]) => {
74
+ map.set(key, value)
75
+ })
76
+ }
65
77
  })
66
78
  })
67
79
  }
68
80
 
69
- getMap(name?: string) {
70
- return this.ydoc.getMap(name)
81
+ getMap() {
82
+ return this.ydoc.getMap()
71
83
  }
72
84
 
73
85
  async hasClient(client: WsServerSocket) {
@@ -83,10 +95,12 @@ export class CuboCrdtDocument {
83
95
  }
84
96
 
85
97
  async write(dto: object) {
98
+ console.log('CRDT write document', this.opts.name, dto)
99
+
86
100
  return new Promise<void>(async (resolve) => {
87
- this.ydoc.transact(({ doc }) => {
88
- const map = doc.getMap()
101
+ const map = this.getMap()
89
102
 
103
+ this.ydoc.transact(() => {
90
104
  Object.entries(dto).forEach(([key, value]) => {
91
105
  map.set(key, value)
92
106
  })