mobx-keystone-yjs 1.5.4 → 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.
Files changed (31) hide show
  1. package/CHANGELOG.md +57 -45
  2. package/dist/mobx-keystone-yjs.esm.js +475 -299
  3. package/dist/mobx-keystone-yjs.esm.mjs +475 -299
  4. package/dist/mobx-keystone-yjs.umd.js +475 -299
  5. package/dist/types/binding/YjsTextModel.d.ts +5 -4
  6. package/dist/types/binding/applyMobxChangeToYjsObject.d.ts +3 -0
  7. package/dist/types/binding/applyYjsEventToMobx.d.ts +8 -0
  8. package/dist/types/binding/bindYjsToMobxKeystone.d.ts +1 -1
  9. package/dist/types/binding/convertJsonToYjsData.d.ts +23 -4
  10. package/dist/types/binding/convertYjsDataToJson.d.ts +1 -1
  11. package/dist/types/binding/resolveYjsPath.d.ts +14 -1
  12. package/dist/types/binding/yjsBindingContext.d.ts +2 -2
  13. package/dist/types/binding/yjsSnapshotTracking.d.ts +24 -0
  14. package/dist/types/index.d.ts +7 -6
  15. package/dist/types/utils/isYjsValueDeleted.d.ts +7 -0
  16. package/package.json +90 -78
  17. package/src/binding/YjsTextModel.ts +280 -247
  18. package/src/binding/applyMobxChangeToYjsObject.ts +77 -0
  19. package/src/binding/applyYjsEventToMobx.ts +173 -0
  20. package/src/binding/bindYjsToMobxKeystone.ts +300 -192
  21. package/src/binding/convertJsonToYjsData.ts +218 -76
  22. package/src/binding/convertYjsDataToJson.ts +1 -1
  23. package/src/binding/resolveYjsPath.ts +51 -27
  24. package/src/binding/yjsSnapshotTracking.ts +40 -0
  25. package/src/index.ts +11 -10
  26. package/src/utils/getOrCreateYjsCollectionAtom.ts +27 -27
  27. package/src/utils/isYjsValueDeleted.ts +14 -0
  28. package/dist/types/binding/applyMobxKeystonePatchToYjsObject.d.ts +0 -2
  29. package/dist/types/binding/convertYjsEventToPatches.d.ts +0 -3
  30. package/src/binding/applyMobxKeystonePatchToYjsObject.ts +0 -98
  31. package/src/binding/convertYjsEventToPatches.ts +0 -92
@@ -1,76 +1,218 @@
1
- import * as Y from "yjs"
2
- import { YjsTextModel, yjsTextModelId } from "./YjsTextModel"
3
- import { SnapshotOutOf } from "mobx-keystone"
4
- import { YjsData } from "./convertYjsDataToJson"
5
- import { PlainArray, PlainObject, PlainPrimitive, PlainValue } from "../plainTypes"
6
- import { action, runInAction } from "mobx"
7
-
8
- function isPlainPrimitive(v: PlainValue): v is PlainPrimitive {
9
- const t = typeof v
10
- return t === "string" || t === "number" || t === "boolean" || v === null || v === undefined
11
- }
12
-
13
- function isPlainArray(v: PlainValue): v is PlainArray {
14
- return Array.isArray(v)
15
- }
16
-
17
- function isPlainObject(v: PlainValue): v is PlainObject {
18
- return !isPlainArray(v) && typeof v === "object" && v !== null
19
- }
20
-
21
- /**
22
- * Converts a plain value to a Y.js data structure.
23
- * Objects are converted to Y.Maps, arrays to Y.Arrays, primitives are untouched.
24
- * Frozen values are a special case and they are kept as immutable plain values.
25
- */
26
- export function convertJsonToYjsData(v: PlainValue): YjsData {
27
- return runInAction(() => {
28
- if (isPlainPrimitive(v)) {
29
- return v
30
- }
31
-
32
- if (isPlainArray(v)) {
33
- const arr = new Y.Array()
34
- applyJsonArrayToYArray(arr, v)
35
- return arr
36
- }
37
-
38
- if (isPlainObject(v)) {
39
- if (v.$frozen === true) {
40
- // frozen value, save as immutable object
41
- return v
42
- }
43
-
44
- if (v.$modelType === yjsTextModelId) {
45
- const text = new Y.Text()
46
- const yjsTextModel = v as unknown as SnapshotOutOf<YjsTextModel>
47
- yjsTextModel.deltaList.forEach((frozenDeltas) => {
48
- text.applyDelta(frozenDeltas.data)
49
- })
50
- return text
51
- }
52
-
53
- const map = new Y.Map()
54
- applyJsonObjectToYMap(map, v)
55
- return map
56
- }
57
-
58
- throw new Error(`unsupported value type: ${v}`)
59
- })
60
- }
61
-
62
- /**
63
- * Applies a JSON array to a Y.Array, using the convertJsonToYjsData to convert the values.
64
- */
65
- export const applyJsonArrayToYArray = action((dest: Y.Array<any>, source: PlainArray) => {
66
- dest.push(source.map(convertJsonToYjsData))
67
- })
68
-
69
- /**
70
- * Applies a JSON object to a Y.Map, using the convertJsonToYjsData to convert the values.
71
- */
72
- export const applyJsonObjectToYMap = action((dest: Y.Map<any>, source: PlainObject) => {
73
- Object.entries(source).forEach(([k, v]) => {
74
- dest.set(k, convertJsonToYjsData(v))
75
- })
76
- })
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,8 +1,8 @@
1
+ import { action } from "mobx"
1
2
  import { modelSnapshotOutWithMetadata } from "mobx-keystone"
2
3
  import * as Y from "yjs"
3
4
  import { PlainObject, PlainValue } from "../plainTypes"
4
5
  import { YjsTextModel } from "./YjsTextModel"
5
- import { action } from "mobx"
6
6
 
7
7
  export type YjsData = Y.Array<any> | Y.Map<any> | Y.Text | PlainValue
8
8
 
@@ -1,27 +1,51 @@
1
- import * as Y from "yjs"
2
- import { failure } from "../utils/error"
3
- import { getOrCreateYjsCollectionAtom } from "../utils/getOrCreateYjsCollectionAtom"
4
-
5
- export function resolveYjsPath(yjsObject: unknown, path: readonly (string | number)[]): unknown {
6
- let currentYjsObject: unknown = yjsObject
7
-
8
- path.forEach((pathPart, i) => {
9
- if (currentYjsObject instanceof Y.Map) {
10
- getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
11
- const key = String(pathPart)
12
- currentYjsObject = currentYjsObject.get(key)
13
- } else if (currentYjsObject instanceof Y.Array) {
14
- getOrCreateYjsCollectionAtom(currentYjsObject).reportObserved()
15
- const key = Number(pathPart)
16
- currentYjsObject = currentYjsObject.get(key)
17
- } else {
18
- throw failure(
19
- `Y.Map or Y.Array was expected at path ${JSON.stringify(
20
- path.slice(0, i)
21
- )} in order to resolve path ${JSON.stringify(path)}, but got ${currentYjsObject} instead`
22
- )
23
- }
24
- })
25
-
26
- return currentYjsObject
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 { YjsTextModel, yjsTextModelId } from "./binding/YjsTextModel"
2
- export { bindYjsToMobxKeystone } from "./binding/bindYjsToMobxKeystone"
3
- export {
4
- applyJsonArrayToYArray,
5
- applyJsonObjectToYMap,
6
- convertJsonToYjsData,
7
- } from "./binding/convertJsonToYjsData"
8
- export { yjsBindingContext } from "./binding/yjsBindingContext"
9
- export type { YjsBindingContext } from "./binding/yjsBindingContext"
10
- export { MobxKeystoneYjsError } from "./utils/error"
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 { IAtom, createAtom } 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
+ 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
+ }
@@ -0,0 +1,14 @@
1
+ import * as Y from "yjs"
2
+
3
+ /**
4
+ * Checks if a Y.js value has been deleted or its document destroyed.
5
+ *
6
+ * @param yjsValue The Y.js value to check.
7
+ * @returns `true` if the value is deleted or destroyed, `false` otherwise.
8
+ */
9
+ export function isYjsValueDeleted(yjsValue: unknown): boolean {
10
+ if (yjsValue instanceof Y.AbstractType) {
11
+ return !!(yjsValue as any)._item?.deleted || !!yjsValue.doc?.isDestroyed
12
+ }
13
+ return false
14
+ }
@@ -1,2 +0,0 @@
1
- import { Patch } from "mobx-keystone";
2
- export declare function applyMobxKeystonePatchToYjsObject(patch: Patch, yjs: unknown): void;
@@ -1,3 +0,0 @@
1
- import { Patch } from "mobx-keystone";
2
- import * as Y from "yjs";
3
- export declare function convertYjsEventToPatches(event: Y.YEvent<any>): Patch[];
@@ -1,98 +0,0 @@
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 { PlainValue } from "../plainTypes"
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 PlainValue))
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.from({ length: toInsert }).fill(undefined))
69
- }
70
- } else {
71
- yjs.delete(Number(key))
72
- yjs.insert(Number(key), [convertJsonToYjsData(patch.value as PlainValue)])
73
- }
74
- break
75
- }
76
- case "add": {
77
- yjs.insert(Number(key), [convertJsonToYjsData(patch.value as PlainValue)])
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
- }