mobx-keystone-yjs 1.3.1 → 1.4.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 +25 -21
- package/dist/mobx-keystone-yjs.esm.js +303 -24
- package/dist/mobx-keystone-yjs.esm.mjs +303 -24
- package/dist/mobx-keystone-yjs.umd.js +302 -25
- package/dist/types/binding/YjsTextModel.d.ts +39 -0
- package/dist/types/binding/bindYjsToMobxKeystone.d.ts +22 -1
- package/dist/types/binding/convertJsonToYjsData.d.ts +13 -2
- package/dist/types/binding/convertYjsDataToJson.d.ts +3 -0
- package/dist/types/binding/resolveYjsPath.d.ts +1 -0
- package/dist/types/binding/yjsBindingContext.d.ts +26 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/utils/getOrCreateYjsCollectionAtom.d.ts +1 -0
- package/package.json +91 -87
- package/src/binding/YjsTextModel.ts +249 -0
- package/src/binding/applyMobxKeystonePatchToYjsObject.ts +96 -92
- package/src/binding/bindYjsToMobxKeystone.ts +191 -157
- package/src/binding/convertJsonToYjsData.ts +72 -50
- package/src/binding/convertYjsDataToJson.ts +31 -0
- package/src/binding/convertYjsEventToPatches.ts +92 -85
- package/src/binding/resolveYjsPath.ts +27 -0
- package/src/binding/yjsBindingContext.ts +42 -12
- package/src/index.ts +10 -9
- package/src/utils/getOrCreateYjsCollectionAtom.ts +18 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { modelSnapshotOutWithMetadata } from "mobx-keystone"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
import { JsonValue } from "../jsonTypes"
|
|
4
|
+
import { YjsTextModel } from "./YjsTextModel"
|
|
5
|
+
|
|
6
|
+
export function convertYjsDataToJson(
|
|
7
|
+
yjsData: Y.Array<unknown> | Y.Map<unknown> | Y.Text | unknown
|
|
8
|
+
): JsonValue {
|
|
9
|
+
if (yjsData instanceof Y.Array) {
|
|
10
|
+
return yjsData.map((v) => convertYjsDataToJson(v))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (yjsData instanceof Y.Map) {
|
|
14
|
+
const obj: Record<string, JsonValue> = {}
|
|
15
|
+
yjsData.forEach((v, k) => {
|
|
16
|
+
obj[k] = convertYjsDataToJson(v)
|
|
17
|
+
})
|
|
18
|
+
return obj
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (yjsData instanceof Y.Text) {
|
|
22
|
+
const deltas = yjsData.toDelta()
|
|
23
|
+
|
|
24
|
+
return modelSnapshotOutWithMetadata(YjsTextModel, {
|
|
25
|
+
deltaList: deltas.length > 0 ? [{ $frozen: true, data: deltas }] : [],
|
|
26
|
+
}) as JsonValue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// assume it's a primitive
|
|
30
|
+
return yjsData as JsonValue
|
|
31
|
+
}
|
|
@@ -1,85 +1,92 @@
|
|
|
1
|
-
import { Patch } from "mobx-keystone"
|
|
2
|
-
import * as Y from "yjs"
|
|
3
|
-
import { JsonArray, JsonObject, JsonValue } from "../jsonTypes"
|
|
4
|
-
import { failure } from "../utils/error"
|
|
5
|
-
|
|
6
|
-
export function convertYjsEventToPatches(event: Y.YEvent<any>): Patch[] {
|
|
7
|
-
const patches: Patch[] = []
|
|
8
|
-
|
|
9
|
-
if (event instanceof Y.YMapEvent) {
|
|
10
|
-
const source = event.target as Y.Map<any>
|
|
11
|
-
|
|
12
|
-
event.changes.keys.forEach((change, key) => {
|
|
13
|
-
const path = [...event.path, key]
|
|
14
|
-
|
|
15
|
-
switch (change.action) {
|
|
16
|
-
case "add":
|
|
17
|
-
patches.push({
|
|
18
|
-
op: "add",
|
|
19
|
-
path,
|
|
20
|
-
value: toPlainValue(source.get(key)),
|
|
21
|
-
})
|
|
22
|
-
break
|
|
23
|
-
|
|
24
|
-
case "update":
|
|
25
|
-
patches.push({
|
|
26
|
-
op: "replace",
|
|
27
|
-
path,
|
|
28
|
-
value: toPlainValue(source.get(key)),
|
|
29
|
-
})
|
|
30
|
-
break
|
|
31
|
-
|
|
32
|
-
case "delete":
|
|
33
|
-
patches.push({
|
|
34
|
-
op: "remove",
|
|
35
|
-
path,
|
|
36
|
-
})
|
|
37
|
-
break
|
|
38
|
-
|
|
39
|
-
default:
|
|
40
|
-
throw failure(`unsupported Yjs map event action: ${change.action}`)
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
} else if (event instanceof Y.YArrayEvent) {
|
|
44
|
-
let retain = 0
|
|
45
|
-
event.changes.delta.forEach((change) => {
|
|
46
|
-
if (change.retain) {
|
|
47
|
-
retain += change.retain
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (change.delete) {
|
|
51
|
-
// remove X items at retain position
|
|
52
|
-
const path = [...event.path, retain]
|
|
53
|
-
for (let i = 0; i < change.delete; i++) {
|
|
54
|
-
patches.push({
|
|
55
|
-
op: "remove",
|
|
56
|
-
path,
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (change.insert) {
|
|
62
|
-
const newValues = Array.isArray(change.insert) ? change.insert : [change.insert]
|
|
63
|
-
newValues.forEach((v) => {
|
|
64
|
-
const path = [...event.path, retain]
|
|
65
|
-
patches.push({
|
|
66
|
-
op: "add",
|
|
67
|
-
path,
|
|
68
|
-
value: toPlainValue(v),
|
|
69
|
-
})
|
|
70
|
-
retain++
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
import { Patch } from "mobx-keystone"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
import { JsonArray, JsonObject, JsonValue } from "../jsonTypes"
|
|
4
|
+
import { failure } from "../utils/error"
|
|
5
|
+
|
|
6
|
+
export function convertYjsEventToPatches(event: Y.YEvent<any>): Patch[] {
|
|
7
|
+
const patches: Patch[] = []
|
|
8
|
+
|
|
9
|
+
if (event instanceof Y.YMapEvent) {
|
|
10
|
+
const source = event.target as Y.Map<any>
|
|
11
|
+
|
|
12
|
+
event.changes.keys.forEach((change, key) => {
|
|
13
|
+
const path = [...event.path, key]
|
|
14
|
+
|
|
15
|
+
switch (change.action) {
|
|
16
|
+
case "add":
|
|
17
|
+
patches.push({
|
|
18
|
+
op: "add",
|
|
19
|
+
path,
|
|
20
|
+
value: toPlainValue(source.get(key)),
|
|
21
|
+
})
|
|
22
|
+
break
|
|
23
|
+
|
|
24
|
+
case "update":
|
|
25
|
+
patches.push({
|
|
26
|
+
op: "replace",
|
|
27
|
+
path,
|
|
28
|
+
value: toPlainValue(source.get(key)),
|
|
29
|
+
})
|
|
30
|
+
break
|
|
31
|
+
|
|
32
|
+
case "delete":
|
|
33
|
+
patches.push({
|
|
34
|
+
op: "remove",
|
|
35
|
+
path,
|
|
36
|
+
})
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
default:
|
|
40
|
+
throw failure(`unsupported Yjs map event action: ${change.action}`)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
} else if (event instanceof Y.YArrayEvent) {
|
|
44
|
+
let retain = 0
|
|
45
|
+
event.changes.delta.forEach((change) => {
|
|
46
|
+
if (change.retain) {
|
|
47
|
+
retain += change.retain
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (change.delete) {
|
|
51
|
+
// remove X items at retain position
|
|
52
|
+
const path = [...event.path, retain]
|
|
53
|
+
for (let i = 0; i < change.delete; i++) {
|
|
54
|
+
patches.push({
|
|
55
|
+
op: "remove",
|
|
56
|
+
path,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (change.insert) {
|
|
62
|
+
const newValues = Array.isArray(change.insert) ? change.insert : [change.insert]
|
|
63
|
+
newValues.forEach((v) => {
|
|
64
|
+
const path = [...event.path, retain]
|
|
65
|
+
patches.push({
|
|
66
|
+
op: "add",
|
|
67
|
+
path,
|
|
68
|
+
value: toPlainValue(v),
|
|
69
|
+
})
|
|
70
|
+
retain++
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
} else if (event instanceof Y.YTextEvent) {
|
|
75
|
+
const path = [...event.path, "deltaList", -1 /* last item */]
|
|
76
|
+
patches.push({
|
|
77
|
+
op: "add",
|
|
78
|
+
path,
|
|
79
|
+
value: { $frozen: true, data: event.delta },
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return patches
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toPlainValue(v: Y.Map<any> | Y.Array<any> | JsonValue) {
|
|
87
|
+
if (v instanceof Y.Map || v instanceof Y.Array) {
|
|
88
|
+
return v.toJSON() as JsonObject | JsonArray
|
|
89
|
+
} else {
|
|
90
|
+
return v
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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,12 +1,42 @@
|
|
|
1
|
-
import { AnyType, createContext } from "mobx-keystone"
|
|
2
|
-
import * as Y from "yjs"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { AnyType, createContext } from "mobx-keystone"
|
|
2
|
+
import * as Y from "yjs"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Context with info on how a mobx-keystone model is bound to a Y.js data structure.
|
|
6
|
+
*/
|
|
7
|
+
export interface YjsBindingContext {
|
|
8
|
+
/**
|
|
9
|
+
* The Y.js document.
|
|
10
|
+
*/
|
|
11
|
+
yjsDoc: Y.Doc
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The bound Y.js data structure.
|
|
15
|
+
*/
|
|
16
|
+
yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The mobx-keystone model type.
|
|
20
|
+
*/
|
|
21
|
+
mobxKeystoneType: AnyType
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The origin symbol used for transactions.
|
|
25
|
+
*/
|
|
26
|
+
yjsOrigin: symbol
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The bound mobx-keystone instance.
|
|
30
|
+
*/
|
|
31
|
+
boundObject: unknown | undefined
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether we are currently applying Y.js changes to the mobx-keystone model.
|
|
35
|
+
*/
|
|
36
|
+
isApplyingYjsChangesToMobxKeystone: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Context with info on how a mobx-keystone model is bound to a Y.js data structure.
|
|
41
|
+
*/
|
|
42
|
+
export const yjsBindingContext = createContext<YjsBindingContext | undefined>(undefined)
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
export {
|
|
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"
|
|
@@ -0,0 +1,18 @@
|
|
|
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 getOrCreateYjsCollectionAtom = (
|
|
10
|
+
yjsCollection: Y.Map<unknown> | Y.Array<unknown>
|
|
11
|
+
): IAtom => {
|
|
12
|
+
let atom = yjsCollectionAtoms.get(yjsCollection)
|
|
13
|
+
if (!atom) {
|
|
14
|
+
atom = createAtom(`yjsCollectionAtom`)
|
|
15
|
+
yjsCollectionAtoms.set(yjsCollection, atom)
|
|
16
|
+
}
|
|
17
|
+
return atom
|
|
18
|
+
}
|