mobx-keystone-yjs 1.4.0 → 1.5.1
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/CHANGELOG.md +33 -25
- package/dist/mobx-keystone-yjs.esm.js +33 -30
- package/dist/mobx-keystone-yjs.esm.mjs +33 -30
- package/dist/mobx-keystone-yjs.umd.js +33 -30
- package/dist/types/binding/bindYjsToMobxKeystone.d.ts +1 -1
- package/dist/types/binding/convertJsonToYjsData.d.ts +5 -4
- package/dist/types/binding/convertYjsDataToJson.d.ts +3 -2
- package/dist/types/binding/yjsBindingContext.d.ts +1 -1
- package/dist/types/jsonTypes.d.ts +5 -5
- package/package.json +91 -91
- package/src/binding/YjsTextModel.ts +248 -249
- package/src/binding/applyMobxKeystonePatchToYjsObject.ts +98 -96
- package/src/binding/bindYjsToMobxKeystone.ts +192 -191
- package/src/binding/convertJsonToYjsData.ts +78 -72
- package/src/binding/convertYjsDataToJson.ts +31 -31
- package/src/binding/convertYjsEventToPatches.ts +96 -92
- package/src/binding/resolveYjsPath.ts +27 -27
- package/src/binding/yjsBindingContext.ts +42 -42
- package/src/jsonTypes.ts +9 -4
- package/src/utils/getOrCreateYjsCollectionAtom.ts +27 -18
|
@@ -1,96 +1,98 @@
|
|
|
1
|
-
import { Patch } from "mobx-keystone"
|
|
2
|
-
import * as Y from "yjs"
|
|
3
|
-
import { failure } from "../utils/error"
|
|
4
|
-
import { convertJsonToYjsData } from "./convertJsonToYjsData"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
case "
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
} else {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
1
|
+
import { Patch } from "mobx-keystone"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
import { failure } from "../utils/error"
|
|
4
|
+
import { convertJsonToYjsData } from "./convertJsonToYjsData"
|
|
5
|
+
import { JsonValueWithUndefined } from "../jsonTypes"
|
|
6
|
+
|
|
7
|
+
export function applyMobxKeystonePatchToYjsObject(patch: Patch, yjs: unknown): void {
|
|
8
|
+
if (patch.path.length > 1) {
|
|
9
|
+
const [key, ...rest] = patch.path
|
|
10
|
+
|
|
11
|
+
if (yjs instanceof Y.Map) {
|
|
12
|
+
const child = yjs.get(String(key)) as unknown
|
|
13
|
+
if (child === undefined) {
|
|
14
|
+
throw failure(
|
|
15
|
+
`invalid patch path, key "${key}" not found in Yjs map - patch: ${JSON.stringify(patch)}`
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
applyMobxKeystonePatchToYjsObject({ ...patch, path: rest }, child)
|
|
19
|
+
} else if (yjs instanceof Y.Array) {
|
|
20
|
+
const child = yjs.get(Number(key)) as unknown
|
|
21
|
+
if (child === undefined) {
|
|
22
|
+
throw failure(
|
|
23
|
+
`invalid patch path, key "${key}" not found in Yjs array - patch: ${JSON.stringify(
|
|
24
|
+
patch
|
|
25
|
+
)}`
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
applyMobxKeystonePatchToYjsObject({ ...patch, path: rest }, child)
|
|
29
|
+
} else if (yjs instanceof Y.Text) {
|
|
30
|
+
// changes to deltaList will be handled by the array observe in the YjsTextModel class
|
|
31
|
+
} else {
|
|
32
|
+
throw failure(
|
|
33
|
+
`invalid patch path, key "${key}" not found in unknown Yjs object - patch: ${JSON.stringify(
|
|
34
|
+
patch
|
|
35
|
+
)}`
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
} else if (patch.path.length === 1) {
|
|
39
|
+
if (yjs instanceof Y.Map) {
|
|
40
|
+
const key = String(patch.path[0])
|
|
41
|
+
|
|
42
|
+
switch (patch.op) {
|
|
43
|
+
case "add":
|
|
44
|
+
case "replace": {
|
|
45
|
+
yjs.set(key, convertJsonToYjsData(patch.value as JsonValueWithUndefined))
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
case "remove": {
|
|
49
|
+
yjs.delete(key)
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
default: {
|
|
53
|
+
throw failure(`invalid patch operation for map`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else if (yjs instanceof Y.Array) {
|
|
57
|
+
const key = patch.path[0]
|
|
58
|
+
|
|
59
|
+
switch (patch.op) {
|
|
60
|
+
case "replace": {
|
|
61
|
+
if (key === "length") {
|
|
62
|
+
const newLength = patch.value as number
|
|
63
|
+
if (yjs.length > newLength) {
|
|
64
|
+
const toDelete = yjs.length - newLength
|
|
65
|
+
yjs.delete(newLength, toDelete)
|
|
66
|
+
} else if (yjs.length < patch.value) {
|
|
67
|
+
const toInsert = patch.value - yjs.length
|
|
68
|
+
yjs.insert(yjs.length, Array(toInsert).fill(undefined))
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
yjs.delete(Number(key))
|
|
72
|
+
yjs.insert(Number(key), [convertJsonToYjsData(patch.value as JsonValueWithUndefined)])
|
|
73
|
+
}
|
|
74
|
+
break
|
|
75
|
+
}
|
|
76
|
+
case "add": {
|
|
77
|
+
yjs.insert(Number(key), [convertJsonToYjsData(patch.value as JsonValueWithUndefined)])
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
case "remove": {
|
|
81
|
+
yjs.delete(Number(key))
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
default: {
|
|
85
|
+
throw failure(`invalid patch operation for array`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (yjs instanceof Y.Text) {
|
|
89
|
+
// initialization of a YjsTextModel, do nothing
|
|
90
|
+
} else {
|
|
91
|
+
throw failure(
|
|
92
|
+
`invalid patch path, the Yjs object is of an unkown type, so key "${String(patch.path[0])}" cannot be found in it`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
throw failure(`invalid patch path, it cannot be empty`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,191 +1,192 @@
|
|
|
1
|
-
import { action } from "mobx"
|
|
2
|
-
import {
|
|
3
|
-
AnyDataModel,
|
|
4
|
-
AnyModel,
|
|
5
|
-
AnyStandardType,
|
|
6
|
-
ModelClass,
|
|
7
|
-
Patch,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
1
|
+
import { action } from "mobx"
|
|
2
|
+
import {
|
|
3
|
+
AnyDataModel,
|
|
4
|
+
AnyModel,
|
|
5
|
+
AnyStandardType,
|
|
6
|
+
ModelClass,
|
|
7
|
+
Patch,
|
|
8
|
+
SnapshotInOf,
|
|
9
|
+
TypeToData,
|
|
10
|
+
applyPatches,
|
|
11
|
+
fromSnapshot,
|
|
12
|
+
getParentToChildPath,
|
|
13
|
+
onGlobalPatches,
|
|
14
|
+
onPatches,
|
|
15
|
+
onSnapshot,
|
|
16
|
+
} from "mobx-keystone"
|
|
17
|
+
import * as Y from "yjs"
|
|
18
|
+
import { getYjsCollectionAtom } from "../utils/getOrCreateYjsCollectionAtom"
|
|
19
|
+
import { applyMobxKeystonePatchToYjsObject } from "./applyMobxKeystonePatchToYjsObject"
|
|
20
|
+
import { YjsData, convertYjsDataToJson } from "./convertYjsDataToJson"
|
|
21
|
+
import { convertYjsEventToPatches } from "./convertYjsEventToPatches"
|
|
22
|
+
import { YjsBindingContext, yjsBindingContext } from "./yjsBindingContext"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a bidirectional binding between a Y.js data structure and a mobx-keystone model.
|
|
26
|
+
*/
|
|
27
|
+
export function bindYjsToMobxKeystone<
|
|
28
|
+
TType extends AnyStandardType | ModelClass<AnyModel> | ModelClass<AnyDataModel>,
|
|
29
|
+
>({
|
|
30
|
+
yjsDoc,
|
|
31
|
+
yjsObject,
|
|
32
|
+
mobxKeystoneType,
|
|
33
|
+
}: {
|
|
34
|
+
/**
|
|
35
|
+
* The Y.js document.
|
|
36
|
+
*/
|
|
37
|
+
yjsDoc: Y.Doc
|
|
38
|
+
/**
|
|
39
|
+
* The bound Y.js data structure.
|
|
40
|
+
*/
|
|
41
|
+
yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text
|
|
42
|
+
/**
|
|
43
|
+
* The mobx-keystone model type.
|
|
44
|
+
*/
|
|
45
|
+
mobxKeystoneType: TType
|
|
46
|
+
}): {
|
|
47
|
+
/**
|
|
48
|
+
* The bound mobx-keystone instance.
|
|
49
|
+
*/
|
|
50
|
+
boundObject: TypeToData<TType>
|
|
51
|
+
/**
|
|
52
|
+
* Disposes the binding.
|
|
53
|
+
*/
|
|
54
|
+
dispose: () => void
|
|
55
|
+
/**
|
|
56
|
+
* The Y.js origin symbol used for binding transactions.
|
|
57
|
+
*/
|
|
58
|
+
yjsOrigin: symbol
|
|
59
|
+
} {
|
|
60
|
+
const yjsOrigin = Symbol("bindYjsToMobxKeystoneTransactionOrigin")
|
|
61
|
+
|
|
62
|
+
let applyingYjsChangesToMobxKeystone = 0
|
|
63
|
+
|
|
64
|
+
const bindingContext: YjsBindingContext = {
|
|
65
|
+
yjsDoc,
|
|
66
|
+
yjsObject,
|
|
67
|
+
mobxKeystoneType,
|
|
68
|
+
yjsOrigin,
|
|
69
|
+
boundObject: undefined, // not yet created
|
|
70
|
+
|
|
71
|
+
get isApplyingYjsChangesToMobxKeystone() {
|
|
72
|
+
return applyingYjsChangesToMobxKeystone > 0
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const yjsJson = convertYjsDataToJson(yjsObject as YjsData)
|
|
77
|
+
|
|
78
|
+
const initializationGlobalPatches: { target: object; patches: Patch[] }[] = []
|
|
79
|
+
|
|
80
|
+
const createBoundObject = () => {
|
|
81
|
+
const disposeOnGlobalPatches = onGlobalPatches((target, patches) => {
|
|
82
|
+
initializationGlobalPatches.push({ target, patches })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const boundObject = yjsBindingContext.apply(
|
|
87
|
+
() => fromSnapshot(mobxKeystoneType, yjsJson as unknown as SnapshotInOf<TypeToData<TType>>),
|
|
88
|
+
bindingContext
|
|
89
|
+
)
|
|
90
|
+
yjsBindingContext.set(boundObject, { ...bindingContext, boundObject })
|
|
91
|
+
return boundObject
|
|
92
|
+
} finally {
|
|
93
|
+
disposeOnGlobalPatches()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const boundObject = createBoundObject()
|
|
98
|
+
|
|
99
|
+
// bind any changes from yjs to mobx-keystone
|
|
100
|
+
const observeDeepCb = action((events: Y.YEvent<any>[]) => {
|
|
101
|
+
const patches: Patch[] = []
|
|
102
|
+
events.forEach((event) => {
|
|
103
|
+
if (event.transaction.origin !== yjsOrigin) {
|
|
104
|
+
patches.push(...convertYjsEventToPatches(event))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (event.target instanceof Y.Map || event.target instanceof Y.Array) {
|
|
108
|
+
getYjsCollectionAtom(event.target)?.reportChanged()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
if (patches.length > 0) {
|
|
113
|
+
applyingYjsChangesToMobxKeystone++
|
|
114
|
+
try {
|
|
115
|
+
applyPatches(boundObject, patches)
|
|
116
|
+
} finally {
|
|
117
|
+
applyingYjsChangesToMobxKeystone--
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
yjsObject.observeDeep(observeDeepCb)
|
|
123
|
+
|
|
124
|
+
// bind any changes from mobx-keystone to yjs
|
|
125
|
+
let pendingArrayOfArrayOfPatches: Patch[][] = []
|
|
126
|
+
const disposeOnPatches = onPatches(boundObject, (patches) => {
|
|
127
|
+
if (applyingYjsChangesToMobxKeystone > 0) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
pendingArrayOfArrayOfPatches.push(patches)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// this is only used so we can transact all patches to the snapshot boundary
|
|
135
|
+
const disposeOnSnapshot = onSnapshot(boundObject, () => {
|
|
136
|
+
if (pendingArrayOfArrayOfPatches.length === 0) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const arrayOfArrayOfPatches = pendingArrayOfArrayOfPatches
|
|
141
|
+
pendingArrayOfArrayOfPatches = []
|
|
142
|
+
|
|
143
|
+
yjsDoc.transact(() => {
|
|
144
|
+
arrayOfArrayOfPatches.forEach((arrayOfPatches) => {
|
|
145
|
+
arrayOfPatches.forEach((patch) => {
|
|
146
|
+
applyMobxKeystonePatchToYjsObject(patch, yjsObject)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
}, yjsOrigin)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// sync initial patches, that might include setting defaults, IDs, etc
|
|
153
|
+
yjsDoc.transact(() => {
|
|
154
|
+
// we need to skip initializations until we hit the initialization of the bound object
|
|
155
|
+
// this is because default objects might be created and initialized before the main object
|
|
156
|
+
// but we just need to catch when those are actually assigned to the bound object
|
|
157
|
+
let boundObjectFound = false
|
|
158
|
+
|
|
159
|
+
initializationGlobalPatches.forEach(({ target, patches }) => {
|
|
160
|
+
if (!boundObjectFound) {
|
|
161
|
+
if (target !== boundObject) {
|
|
162
|
+
return // skip
|
|
163
|
+
}
|
|
164
|
+
boundObjectFound = true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const parentToChildPath = getParentToChildPath(boundObject, target)
|
|
168
|
+
// this is undefined only if target is not a child of boundModel
|
|
169
|
+
if (parentToChildPath !== undefined) {
|
|
170
|
+
patches.forEach((patch) => {
|
|
171
|
+
applyMobxKeystonePatchToYjsObject(
|
|
172
|
+
{
|
|
173
|
+
...patch,
|
|
174
|
+
path: [...parentToChildPath, ...patch.path],
|
|
175
|
+
},
|
|
176
|
+
yjsObject
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}, yjsOrigin)
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
boundObject,
|
|
185
|
+
dispose: () => {
|
|
186
|
+
disposeOnPatches()
|
|
187
|
+
disposeOnSnapshot()
|
|
188
|
+
yjsObject.unobserveDeep(observeDeepCb)
|
|
189
|
+
},
|
|
190
|
+
yjsOrigin,
|
|
191
|
+
}
|
|
192
|
+
}
|