mobx-keystone-yjs 1.5.5 → 1.6.0
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 +57 -52
- package/dist/mobx-keystone-yjs.esm.js +334 -216
- package/dist/mobx-keystone-yjs.esm.mjs +334 -216
- package/dist/mobx-keystone-yjs.umd.js +332 -214
- package/dist/types/binding/applyMobxChangeToYjsObject.d.ts +3 -0
- package/dist/types/binding/applyYjsEventToMobx.d.ts +8 -0
- package/dist/types/binding/convertJsonToYjsData.d.ts +22 -3
- package/dist/types/binding/resolveYjsPath.d.ts +14 -1
- package/dist/types/binding/yjsSnapshotTracking.d.ts +24 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +90 -88
- package/src/binding/applyMobxChangeToYjsObject.ts +77 -0
- package/src/binding/applyYjsEventToMobx.ts +173 -0
- package/src/binding/bindYjsToMobxKeystone.ts +300 -202
- package/src/binding/convertJsonToYjsData.ts +218 -73
- package/src/binding/resolveYjsPath.ts +51 -27
- package/src/binding/yjsSnapshotTracking.ts +40 -0
- package/src/index.ts +11 -10
- package/src/utils/getOrCreateYjsCollectionAtom.ts +27 -27
- package/dist/types/binding/applyMobxKeystonePatchToYjsObject.d.ts +0 -2
- package/dist/types/binding/convertYjsEventToPatches.d.ts +0 -3
- package/src/binding/applyMobxKeystonePatchToYjsObject.ts +0 -103
- package/src/binding/convertYjsEventToPatches.ts +0 -84
|
@@ -1,73 +1,218 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { YjsData } from "./convertYjsDataToJson"
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
import { frozenKey, modelTypeKey, SnapshotOutOf } from "mobx-keystone"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
import { PlainArray, PlainObject, PlainPrimitive, PlainValue } from "../plainTypes"
|
|
4
|
+
import { YjsData } from "./convertYjsDataToJson"
|
|
5
|
+
import { YjsTextModel, yjsTextModelId } from "./YjsTextModel"
|
|
6
|
+
import { isYjsContainerUpToDate, setYjsContainerSnapshot } from "./yjsSnapshotTracking"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for applying JSON data to Y.js data structures.
|
|
10
|
+
*/
|
|
11
|
+
export interface ApplyJsonToYjsOptions {
|
|
12
|
+
/**
|
|
13
|
+
* The mode to use when applying JSON data to Y.js data structures.
|
|
14
|
+
* - `add`: Creates new Y.js containers for objects/arrays (default, backwards compatible)
|
|
15
|
+
* - `merge`: Recursively merges values, preserving existing container references where possible
|
|
16
|
+
*/
|
|
17
|
+
mode?: "add" | "merge"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPlainPrimitive(v: PlainValue): v is PlainPrimitive {
|
|
21
|
+
const t = typeof v
|
|
22
|
+
return t === "string" || t === "number" || t === "boolean" || v === null || v === undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPlainArray(v: PlainValue): v is PlainArray {
|
|
26
|
+
return Array.isArray(v)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPlainObject(v: PlainValue): v is PlainObject {
|
|
30
|
+
return typeof v === "object" && v !== null && !Array.isArray(v)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Converts a plain value to a Y.js data structure.
|
|
35
|
+
* Objects are converted to Y.Maps, arrays to Y.Arrays, primitives are untouched.
|
|
36
|
+
* Frozen values are a special case and they are kept as immutable plain values.
|
|
37
|
+
*/
|
|
38
|
+
export function convertJsonToYjsData(v: PlainValue): YjsData {
|
|
39
|
+
if (isPlainPrimitive(v)) {
|
|
40
|
+
return v
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isPlainArray(v)) {
|
|
44
|
+
const arr = new Y.Array()
|
|
45
|
+
applyJsonArrayToYArray(arr, v)
|
|
46
|
+
return arr
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isPlainObject(v)) {
|
|
50
|
+
if (v[frozenKey] === true) {
|
|
51
|
+
// frozen value, save as immutable object
|
|
52
|
+
return v
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (v[modelTypeKey] === yjsTextModelId) {
|
|
56
|
+
const text = new Y.Text()
|
|
57
|
+
const yjsTextModel = v as unknown as SnapshotOutOf<YjsTextModel>
|
|
58
|
+
yjsTextModel.deltaList.forEach((frozenDeltas) => {
|
|
59
|
+
text.applyDelta(frozenDeltas.data)
|
|
60
|
+
})
|
|
61
|
+
return text
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const map = new Y.Map()
|
|
65
|
+
applyJsonObjectToYMap(map, v)
|
|
66
|
+
return map
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new Error(`unsupported value type: ${v}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Applies a JSON array to a Y.Array, using the convertJsonToYjsData to convert the values.
|
|
74
|
+
*
|
|
75
|
+
* @param dest The destination Y.Array.
|
|
76
|
+
* @param source The source JSON array.
|
|
77
|
+
* @param options Options for applying the JSON data.
|
|
78
|
+
*/
|
|
79
|
+
export const applyJsonArrayToYArray = (
|
|
80
|
+
dest: Y.Array<any>,
|
|
81
|
+
source: PlainArray,
|
|
82
|
+
options: ApplyJsonToYjsOptions = {}
|
|
83
|
+
) => {
|
|
84
|
+
const { mode = "add" } = options
|
|
85
|
+
|
|
86
|
+
// In merge mode, check if the container is already up-to-date with this snapshot
|
|
87
|
+
if (mode === "merge" && isYjsContainerUpToDate(dest, source)) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const srcLen = source.length
|
|
92
|
+
|
|
93
|
+
if (mode === "add") {
|
|
94
|
+
// Add mode: just push all items to the end
|
|
95
|
+
for (let i = 0; i < srcLen; i++) {
|
|
96
|
+
dest.push([convertJsonToYjsData(source[i])])
|
|
97
|
+
}
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Merge mode: recursively merge values, preserving existing container references
|
|
102
|
+
const destLen = dest.length
|
|
103
|
+
|
|
104
|
+
// Remove extra items from the end
|
|
105
|
+
if (destLen > srcLen) {
|
|
106
|
+
dest.delete(srcLen, destLen - srcLen)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Update existing items
|
|
110
|
+
const minLen = Math.min(destLen, srcLen)
|
|
111
|
+
for (let i = 0; i < minLen; i++) {
|
|
112
|
+
const srcItem = source[i]
|
|
113
|
+
const destItem = dest.get(i)
|
|
114
|
+
|
|
115
|
+
// If both are objects, merge recursively
|
|
116
|
+
if (isPlainObject(srcItem) && destItem instanceof Y.Map) {
|
|
117
|
+
applyJsonObjectToYMap(destItem, srcItem, options)
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If both are arrays, merge recursively
|
|
122
|
+
if (isPlainArray(srcItem) && destItem instanceof Y.Array) {
|
|
123
|
+
applyJsonArrayToYArray(destItem, srcItem, options)
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Skip if primitive value is unchanged (optimization)
|
|
128
|
+
if (isPlainPrimitive(srcItem) && destItem === srcItem) {
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Otherwise, replace the item
|
|
133
|
+
dest.delete(i, 1)
|
|
134
|
+
dest.insert(i, [convertJsonToYjsData(srcItem)])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add new items at the end
|
|
138
|
+
for (let i = destLen; i < srcLen; i++) {
|
|
139
|
+
dest.push([convertJsonToYjsData(source[i])])
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Update snapshot tracking after successful merge
|
|
143
|
+
setYjsContainerSnapshot(dest, source)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Applies a JSON object to a Y.Map, using the convertJsonToYjsData to convert the values.
|
|
148
|
+
*
|
|
149
|
+
* @param dest The destination Y.Map.
|
|
150
|
+
* @param source The source JSON object.
|
|
151
|
+
* @param options Options for applying the JSON data.
|
|
152
|
+
*/
|
|
153
|
+
export const applyJsonObjectToYMap = (
|
|
154
|
+
dest: Y.Map<any>,
|
|
155
|
+
source: PlainObject,
|
|
156
|
+
options: ApplyJsonToYjsOptions = {}
|
|
157
|
+
) => {
|
|
158
|
+
const { mode = "add" } = options
|
|
159
|
+
|
|
160
|
+
// In merge mode, check if the container is already up-to-date with this snapshot
|
|
161
|
+
if (mode === "merge" && isYjsContainerUpToDate(dest, source)) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (mode === "add") {
|
|
166
|
+
// Add mode: just set all values
|
|
167
|
+
for (const k of Object.keys(source)) {
|
|
168
|
+
const v = source[k]
|
|
169
|
+
if (v !== undefined) {
|
|
170
|
+
dest.set(k, convertJsonToYjsData(v))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Merge mode: recursively merge values, preserving existing container references
|
|
177
|
+
|
|
178
|
+
// Delete keys that are not present in source (or have undefined value)
|
|
179
|
+
const sourceKeysWithValues = new Set(Object.keys(source).filter((k) => source[k] !== undefined))
|
|
180
|
+
for (const key of dest.keys()) {
|
|
181
|
+
if (!sourceKeysWithValues.has(key)) {
|
|
182
|
+
dest.delete(key)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const k of Object.keys(source)) {
|
|
187
|
+
const v = source[k]
|
|
188
|
+
// Skip undefined values - Y.js maps cannot store undefined
|
|
189
|
+
if (v === undefined) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const existing = dest.get(k)
|
|
194
|
+
|
|
195
|
+
// If source is an object and dest has a Y.Map, merge recursively
|
|
196
|
+
if (isPlainObject(v) && existing instanceof Y.Map) {
|
|
197
|
+
applyJsonObjectToYMap(existing, v, options)
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If source is an array and dest has a Y.Array, merge recursively
|
|
202
|
+
if (isPlainArray(v) && existing instanceof Y.Array) {
|
|
203
|
+
applyJsonArrayToYArray(existing, v, options)
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Skip if primitive value is unchanged (optimization)
|
|
208
|
+
if (isPlainPrimitive(v) && existing === v) {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Otherwise, convert and set the value (this creates new containers if needed)
|
|
213
|
+
dest.set(k, convertJsonToYjsData(v))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Update snapshot tracking after successful merge
|
|
217
|
+
setYjsContainerSnapshot(dest, source)
|
|
218
|
+
}
|
|
@@ -1,27 +1,51 @@
|
|
|
1
|
-
import * as Y from "yjs"
|
|
2
|
-
import { failure } from "../utils/error"
|
|
3
|
-
import { getOrCreateYjsCollectionAtom } from "../utils/getOrCreateYjsCollectionAtom"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import * as Y from "yjs"
|
|
2
|
+
import { failure } from "../utils/error"
|
|
3
|
+
import { getOrCreateYjsCollectionAtom } from "../utils/getOrCreateYjsCollectionAtom"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a path within a Yjs object structure.
|
|
7
|
+
* Returns the Yjs container at the specified path.
|
|
8
|
+
*
|
|
9
|
+
* When a Y.Text is encountered during path resolution (either at the start
|
|
10
|
+
* or mid-path), it is returned immediately since Y.Text doesn't support
|
|
11
|
+
* nested path traversal.
|
|
12
|
+
*
|
|
13
|
+
* @param yjsObject The root Yjs object
|
|
14
|
+
* @param path Array of keys/indices to traverse
|
|
15
|
+
* @returns The Yjs container at the path, or Y.Text if encountered during traversal
|
|
16
|
+
*/
|
|
17
|
+
export function resolveYjsPath(
|
|
18
|
+
yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text,
|
|
19
|
+
path: readonly (string | number)[]
|
|
20
|
+
): unknown {
|
|
21
|
+
let currentYjsObject: unknown = yjsObject
|
|
22
|
+
|
|
23
|
+
let i = -1
|
|
24
|
+
for (const pathPart of path) {
|
|
25
|
+
i++
|
|
26
|
+
// If we encounter a Y.Text during path resolution, return it immediately.
|
|
27
|
+
// Y.Text objects don't support nested path traversal, and their updates
|
|
28
|
+
// are handled separately by YjsTextModel's own synchronization mechanism.
|
|
29
|
+
if (currentYjsObject instanceof Y.Text) {
|
|
30
|
+
return currentYjsObject
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (currentYjsObject instanceof Y.Map) {
|
|
34
|
+
getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
|
|
35
|
+
const key = String(pathPart)
|
|
36
|
+
currentYjsObject = currentYjsObject.get(key)
|
|
37
|
+
} else if (currentYjsObject instanceof Y.Array) {
|
|
38
|
+
getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
|
|
39
|
+
const key = Number(pathPart)
|
|
40
|
+
currentYjsObject = currentYjsObject.get(key)
|
|
41
|
+
} else {
|
|
42
|
+
throw failure(
|
|
43
|
+
`Y.Map or Y.Array was expected at path ${JSON.stringify(
|
|
44
|
+
path.slice(0, i)
|
|
45
|
+
)} in order to resolve path ${JSON.stringify(path)}, but got ${currentYjsObject} instead`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return currentYjsObject
|
|
51
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Y from "yjs"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WeakMap that tracks which snapshot each Y.js container was last synced from.
|
|
5
|
+
* This is used during reconciliation to skip containers that are already up-to-date.
|
|
6
|
+
*
|
|
7
|
+
* The key is the Y.js container (Y.Map or Y.Array).
|
|
8
|
+
* The value is the snapshot (plain object or array) that was last synced to it.
|
|
9
|
+
*/
|
|
10
|
+
export const yjsContainerToSnapshot = new WeakMap<Y.Map<any> | Y.Array<any>, unknown>()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Updates the snapshot tracking for a Y.js container.
|
|
14
|
+
* Call this after syncing a snapshot to a Y.js container.
|
|
15
|
+
*/
|
|
16
|
+
export function setYjsContainerSnapshot(
|
|
17
|
+
container: Y.Map<any> | Y.Array<any>,
|
|
18
|
+
snapshot: unknown
|
|
19
|
+
): void {
|
|
20
|
+
yjsContainerToSnapshot.set(container, snapshot)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the last synced snapshot for a Y.js container.
|
|
25
|
+
* Returns undefined if the container has never been synced.
|
|
26
|
+
*/
|
|
27
|
+
export function getYjsContainerSnapshot(container: Y.Map<any> | Y.Array<any>): unknown {
|
|
28
|
+
return yjsContainerToSnapshot.get(container)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a Y.js container is up-to-date with the given snapshot.
|
|
33
|
+
* Uses reference equality to check if the snapshot is the same.
|
|
34
|
+
*/
|
|
35
|
+
export function isYjsContainerUpToDate(
|
|
36
|
+
container: Y.Map<any> | Y.Array<any>,
|
|
37
|
+
snapshot: unknown
|
|
38
|
+
): boolean {
|
|
39
|
+
return yjsContainerToSnapshot.get(container) === snapshot
|
|
40
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export { bindYjsToMobxKeystone } from "./binding/bindYjsToMobxKeystone"
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
1
|
+
export { bindYjsToMobxKeystone } from "./binding/bindYjsToMobxKeystone"
|
|
2
|
+
export type { ApplyJsonToYjsOptions } from "./binding/convertJsonToYjsData"
|
|
3
|
+
export {
|
|
4
|
+
applyJsonArrayToYArray,
|
|
5
|
+
applyJsonObjectToYMap,
|
|
6
|
+
convertJsonToYjsData,
|
|
7
|
+
} from "./binding/convertJsonToYjsData"
|
|
8
|
+
export { YjsTextModel, yjsTextModelId } from "./binding/YjsTextModel"
|
|
9
|
+
export type { YjsBindingContext } from "./binding/yjsBindingContext"
|
|
10
|
+
export { yjsBindingContext } from "./binding/yjsBindingContext"
|
|
11
|
+
export { MobxKeystoneYjsError } from "./utils/error"
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as Y from "yjs"
|
|
3
|
-
|
|
4
|
-
const yjsCollectionAtoms = new WeakMap<Y.Map<unknown> | Y.Array<unknown>, IAtom>()
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @internal
|
|
8
|
-
*/
|
|
9
|
-
export const getYjsCollectionAtom = (
|
|
10
|
-
yjsCollection: Y.Map<unknown> | Y.Array<unknown>
|
|
11
|
-
): IAtom | undefined => {
|
|
12
|
-
return yjsCollectionAtoms.get(yjsCollection)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @internal
|
|
17
|
-
*/
|
|
18
|
-
export const getOrCreateYjsCollectionAtom = (
|
|
19
|
-
yjsCollection: Y.Map<unknown> | Y.Array<unknown>
|
|
20
|
-
): IAtom => {
|
|
21
|
-
let atom = yjsCollectionAtoms.get(yjsCollection)
|
|
22
|
-
if (!atom) {
|
|
23
|
-
atom = createAtom(`yjsCollectionAtom`)
|
|
24
|
-
yjsCollectionAtoms.set(yjsCollection, atom)
|
|
25
|
-
}
|
|
26
|
-
return atom
|
|
27
|
-
}
|
|
1
|
+
import { createAtom, IAtom } from "mobx"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
|
|
4
|
+
const yjsCollectionAtoms = new WeakMap<Y.Map<unknown> | Y.Array<unknown>, IAtom>()
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const getYjsCollectionAtom = (
|
|
10
|
+
yjsCollection: Y.Map<unknown> | Y.Array<unknown>
|
|
11
|
+
): IAtom | undefined => {
|
|
12
|
+
return yjsCollectionAtoms.get(yjsCollection)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export const getOrCreateYjsCollectionAtom = (
|
|
19
|
+
yjsCollection: Y.Map<unknown> | Y.Array<unknown>
|
|
20
|
+
): IAtom => {
|
|
21
|
+
let atom = yjsCollectionAtoms.get(yjsCollection)
|
|
22
|
+
if (!atom) {
|
|
23
|
+
atom = createAtom(`yjsCollectionAtom`)
|
|
24
|
+
yjsCollectionAtoms.set(yjsCollection, atom)
|
|
25
|
+
}
|
|
26
|
+
return atom
|
|
27
|
+
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { Patch } from "mobx-keystone"
|
|
2
|
-
import * as Y from "yjs"
|
|
3
|
-
import { PlainValue } from "../plainTypes"
|
|
4
|
-
import { failure } from "../utils/error"
|
|
5
|
-
import { isYjsValueDeleted } from "../utils/isYjsValueDeleted"
|
|
6
|
-
import { convertJsonToYjsData } from "./convertJsonToYjsData"
|
|
7
|
-
|
|
8
|
-
export function applyMobxKeystonePatchToYjsObject(patch: Patch, yjs: unknown): void {
|
|
9
|
-
if (isYjsValueDeleted(yjs)) {
|
|
10
|
-
throw failure("cannot apply patch to deleted Yjs value")
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (patch.path.length > 1) {
|
|
14
|
-
const [key, ...rest] = patch.path
|
|
15
|
-
|
|
16
|
-
if (yjs instanceof Y.Map) {
|
|
17
|
-
const child = yjs.get(String(key)) as unknown
|
|
18
|
-
if (child === undefined) {
|
|
19
|
-
throw failure(
|
|
20
|
-
`invalid patch path, key "${key}" not found in Yjs map - patch: ${JSON.stringify(patch)}`
|
|
21
|
-
)
|
|
22
|
-
}
|
|
23
|
-
applyMobxKeystonePatchToYjsObject({ ...patch, path: rest }, child)
|
|
24
|
-
} else if (yjs instanceof Y.Array) {
|
|
25
|
-
const child = yjs.get(Number(key)) as unknown
|
|
26
|
-
if (child === undefined) {
|
|
27
|
-
throw failure(
|
|
28
|
-
`invalid patch path, key "${key}" not found in Yjs array - patch: ${JSON.stringify(
|
|
29
|
-
patch
|
|
30
|
-
)}`
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
applyMobxKeystonePatchToYjsObject({ ...patch, path: rest }, child)
|
|
34
|
-
} else if (yjs instanceof Y.Text) {
|
|
35
|
-
// changes to deltaList will be handled by the array observe in the YjsTextModel class
|
|
36
|
-
} else {
|
|
37
|
-
throw failure(
|
|
38
|
-
`invalid patch path, key "${key}" not found in unknown Yjs object - patch: ${JSON.stringify(
|
|
39
|
-
patch
|
|
40
|
-
)}`
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
} else if (patch.path.length === 1) {
|
|
44
|
-
if (yjs instanceof Y.Map) {
|
|
45
|
-
const key = String(patch.path[0])
|
|
46
|
-
|
|
47
|
-
switch (patch.op) {
|
|
48
|
-
case "add":
|
|
49
|
-
case "replace": {
|
|
50
|
-
yjs.set(key, convertJsonToYjsData(patch.value as PlainValue))
|
|
51
|
-
break
|
|
52
|
-
}
|
|
53
|
-
case "remove": {
|
|
54
|
-
yjs.delete(key)
|
|
55
|
-
break
|
|
56
|
-
}
|
|
57
|
-
default: {
|
|
58
|
-
throw failure(`invalid patch operation for map`)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
} else if (yjs instanceof Y.Array) {
|
|
62
|
-
const key = patch.path[0]
|
|
63
|
-
|
|
64
|
-
switch (patch.op) {
|
|
65
|
-
case "replace": {
|
|
66
|
-
if (key === "length") {
|
|
67
|
-
const newLength = patch.value as number
|
|
68
|
-
if (yjs.length > newLength) {
|
|
69
|
-
const toDelete = yjs.length - newLength
|
|
70
|
-
yjs.delete(newLength, toDelete)
|
|
71
|
-
} else if (yjs.length < patch.value) {
|
|
72
|
-
const toInsert = patch.value - yjs.length
|
|
73
|
-
yjs.insert(yjs.length, Array.from({ length: toInsert }).fill(undefined))
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
yjs.delete(Number(key))
|
|
77
|
-
yjs.insert(Number(key), [convertJsonToYjsData(patch.value as PlainValue)])
|
|
78
|
-
}
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
case "add": {
|
|
82
|
-
yjs.insert(Number(key), [convertJsonToYjsData(patch.value as PlainValue)])
|
|
83
|
-
break
|
|
84
|
-
}
|
|
85
|
-
case "remove": {
|
|
86
|
-
yjs.delete(Number(key))
|
|
87
|
-
break
|
|
88
|
-
}
|
|
89
|
-
default: {
|
|
90
|
-
throw failure(`invalid patch operation for array`)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} else if (yjs instanceof Y.Text) {
|
|
94
|
-
// initialization of a YjsTextModel, do nothing
|
|
95
|
-
} else {
|
|
96
|
-
throw failure(
|
|
97
|
-
`invalid patch path, the Yjs object is of an unkown type, so key "${String(patch.path[0])}" cannot be found in it`
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
throw failure(`invalid patch path, it cannot be empty`)
|
|
102
|
-
}
|
|
103
|
-
}
|