mobx-keystone-yjs 1.0.0 → 1.2.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.
@@ -1,142 +1,156 @@
1
- import {
2
- AnyDataModel,
3
- AnyModel,
4
- AnyStandardType,
5
- ModelClass,
6
- Patch,
7
- TypeToData,
8
- applyPatches,
9
- fromSnapshot,
10
- getParentToChildPath,
11
- onGlobalPatches,
12
- onPatches,
13
- onSnapshot,
14
- } from "mobx-keystone"
15
- import * as Y from "yjs"
16
- import { applyMobxKeystonePatchToYjsObject } from "./applyMobxKeystonePatchToYjsObject"
17
- import { convertYjsEventToPatches } from "./convertYjsEventToPatches"
18
-
19
- export function bindYjsToMobxKeystone<
20
- TType extends AnyStandardType | ModelClass<AnyModel> | ModelClass<AnyDataModel>,
21
- >({
22
- yjsDoc,
23
- yjsObject,
24
- mobxKeystoneType,
25
- }: {
26
- yjsDoc: Y.Doc
27
- yjsObject: Y.Map<unknown> | Y.Array<unknown>
28
- mobxKeystoneType: TType
29
- }): {
30
- boundObject: TypeToData<TType>
31
- dispose(): void
32
- yjsOrigin: symbol
33
- } {
34
- const yjsJson = yjsObject.toJSON()
35
-
36
- const initializationGlobalPatches: { target: object; patches: Patch[] }[] = []
37
-
38
- const createBoundObject = () => {
39
- const disposeOnGlobalPatches = onGlobalPatches((target, patches) => {
40
- initializationGlobalPatches.push({ target, patches })
41
- })
42
-
43
- try {
44
- return fromSnapshot(mobxKeystoneType, yjsJson as any)
45
- } finally {
46
- disposeOnGlobalPatches()
47
- }
48
- }
49
-
50
- const boundObject = createBoundObject()
51
-
52
- let applyingMobxKeystoneChanges = 0
53
- const yjsOrigin = Symbol("bindYjsToMobxKeystoneTransactionOrigin")
54
-
55
- // bind any changes from yjs to mobx-keystone
56
- const observeDeepCb = (events: Y.YEvent<any>[]) => {
57
- const patches: Patch[] = []
58
- events.forEach((event) => {
59
- if (event.transaction.origin !== yjsOrigin) {
60
- patches.push(...convertYjsEventToPatches(event))
61
- }
62
- })
63
-
64
- if (patches.length > 0) {
65
- applyingMobxKeystoneChanges++
66
- try {
67
- applyPatches(boundObject, patches)
68
- } finally {
69
- applyingMobxKeystoneChanges--
70
- }
71
- }
72
- }
73
-
74
- yjsObject.observeDeep(observeDeepCb)
75
-
76
- // bind any changes from mobx-keystone to yjs
77
- let pendingPatches: Patch[] = []
78
- const disposeOnPatches = onPatches(boundObject, (patches) => {
79
- if (applyingMobxKeystoneChanges > 0) {
80
- return
81
- }
82
-
83
- pendingPatches.push(...patches)
84
- })
85
-
86
- // this is only used so we can transact all patches to the snapshot boundary
87
- const disposeOnSnapshot = onSnapshot(boundObject, () => {
88
- if (pendingPatches.length === 0) {
89
- return
90
- }
91
-
92
- const patches = pendingPatches
93
- pendingPatches = []
94
-
95
- yjsDoc.transact(() => {
96
- patches.forEach((patch) => {
97
- applyMobxKeystonePatchToYjsObject(patch, yjsObject)
98
- })
99
- }, yjsOrigin)
100
- })
101
-
102
- // sync initial patches, that might include setting defaults, IDs, etc
103
- yjsDoc.transact(() => {
104
- // we need to skip initializations until we hit the initialization of the bound object
105
- // this is because default objects might be created and initialized before the main object
106
- // but we just need to catch when those are actually assigned to the bound object
107
- let boundObjectFound = false
108
-
109
- initializationGlobalPatches.forEach(({ target, patches }) => {
110
- if (!boundObjectFound) {
111
- if (target !== boundObject) {
112
- return // skip
113
- }
114
- boundObjectFound = true
115
- }
116
-
117
- const parentToChildPath = getParentToChildPath(boundObject, target)
118
- // this is undefined only if target is not a child of boundModel
119
- if (parentToChildPath !== undefined) {
120
- patches.forEach((patch) => {
121
- applyMobxKeystonePatchToYjsObject(
122
- {
123
- ...patch,
124
- path: [...parentToChildPath, ...patch.path],
125
- },
126
- yjsObject
127
- )
128
- })
129
- }
130
- })
131
- }, yjsOrigin)
132
-
133
- return {
134
- boundObject,
135
- dispose: () => {
136
- disposeOnPatches()
137
- disposeOnSnapshot()
138
- yjsObject.unobserveDeep(observeDeepCb)
139
- },
140
- yjsOrigin,
141
- }
142
- }
1
+ import {
2
+ AnyDataModel,
3
+ AnyModel,
4
+ AnyStandardType,
5
+ ModelClass,
6
+ Patch,
7
+ TypeToData,
8
+ applyPatches,
9
+ fromSnapshot,
10
+ getParentToChildPath,
11
+ onGlobalPatches,
12
+ onPatches,
13
+ onSnapshot,
14
+ } from "mobx-keystone"
15
+ import * as Y from "yjs"
16
+ import { applyMobxKeystonePatchToYjsObject } from "./applyMobxKeystonePatchToYjsObject"
17
+ import { convertYjsEventToPatches } from "./convertYjsEventToPatches"
18
+ import { YjsBindingContext, yjsBindingContext } from "./yjsBindingContext"
19
+
20
+ export function bindYjsToMobxKeystone<
21
+ TType extends AnyStandardType | ModelClass<AnyModel> | ModelClass<AnyDataModel>,
22
+ >({
23
+ yjsDoc,
24
+ yjsObject,
25
+ mobxKeystoneType,
26
+ }: {
27
+ yjsDoc: Y.Doc
28
+ yjsObject: Y.Map<unknown> | Y.Array<unknown>
29
+ mobxKeystoneType: TType
30
+ }): {
31
+ boundObject: TypeToData<TType>
32
+ dispose(): void
33
+ yjsOrigin: symbol
34
+ } {
35
+ const yjsOrigin = Symbol("bindYjsToMobxKeystoneTransactionOrigin")
36
+
37
+ const bindingContext: YjsBindingContext = {
38
+ yjsDoc,
39
+ yjsObject,
40
+ mobxKeystoneType,
41
+ yjsOrigin,
42
+ }
43
+
44
+ const yjsJson = yjsObject.toJSON()
45
+
46
+ const initializationGlobalPatches: { target: object; patches: Patch[] }[] = []
47
+
48
+ const createBoundObject = () => {
49
+ const disposeOnGlobalPatches = onGlobalPatches((target, patches) => {
50
+ initializationGlobalPatches.push({ target, patches })
51
+ })
52
+
53
+ try {
54
+ const boundObject = yjsBindingContext.apply(
55
+ () => fromSnapshot(mobxKeystoneType, yjsJson as any),
56
+ bindingContext
57
+ )
58
+ yjsBindingContext.set(boundObject, bindingContext)
59
+ return boundObject
60
+ } finally {
61
+ disposeOnGlobalPatches()
62
+ }
63
+ }
64
+
65
+ const boundObject = createBoundObject()
66
+
67
+ let applyingMobxKeystoneChanges = 0
68
+
69
+ // bind any changes from yjs to mobx-keystone
70
+ const observeDeepCb = (events: Y.YEvent<any>[]) => {
71
+ const patches: Patch[] = []
72
+ events.forEach((event) => {
73
+ if (event.transaction.origin !== yjsOrigin) {
74
+ patches.push(...convertYjsEventToPatches(event))
75
+ }
76
+ })
77
+
78
+ if (patches.length > 0) {
79
+ applyingMobxKeystoneChanges++
80
+ try {
81
+ applyPatches(boundObject, patches)
82
+ } finally {
83
+ applyingMobxKeystoneChanges--
84
+ }
85
+ }
86
+ }
87
+
88
+ yjsObject.observeDeep(observeDeepCb)
89
+
90
+ // bind any changes from mobx-keystone to yjs
91
+ let pendingPatches: Patch[] = []
92
+ const disposeOnPatches = onPatches(boundObject, (patches) => {
93
+ if (applyingMobxKeystoneChanges > 0) {
94
+ return
95
+ }
96
+
97
+ pendingPatches.push(...patches)
98
+ })
99
+
100
+ // this is only used so we can transact all patches to the snapshot boundary
101
+ const disposeOnSnapshot = onSnapshot(boundObject, () => {
102
+ if (pendingPatches.length === 0) {
103
+ return
104
+ }
105
+
106
+ const patches = pendingPatches
107
+ pendingPatches = []
108
+
109
+ yjsDoc.transact(() => {
110
+ patches.forEach((patch) => {
111
+ applyMobxKeystonePatchToYjsObject(patch, yjsObject)
112
+ })
113
+ }, yjsOrigin)
114
+ })
115
+
116
+ // sync initial patches, that might include setting defaults, IDs, etc
117
+ yjsDoc.transact(() => {
118
+ // we need to skip initializations until we hit the initialization of the bound object
119
+ // this is because default objects might be created and initialized before the main object
120
+ // but we just need to catch when those are actually assigned to the bound object
121
+ let boundObjectFound = false
122
+
123
+ initializationGlobalPatches.forEach(({ target, patches }) => {
124
+ if (!boundObjectFound) {
125
+ if (target !== boundObject) {
126
+ return // skip
127
+ }
128
+ boundObjectFound = true
129
+ }
130
+
131
+ const parentToChildPath = getParentToChildPath(boundObject, target)
132
+ // this is undefined only if target is not a child of boundModel
133
+ if (parentToChildPath !== undefined) {
134
+ patches.forEach((patch) => {
135
+ applyMobxKeystonePatchToYjsObject(
136
+ {
137
+ ...patch,
138
+ path: [...parentToChildPath, ...patch.path],
139
+ },
140
+ yjsObject
141
+ )
142
+ })
143
+ }
144
+ })
145
+ }, yjsOrigin)
146
+
147
+ return {
148
+ boundObject,
149
+ dispose: () => {
150
+ disposeOnPatches()
151
+ disposeOnSnapshot()
152
+ yjsObject.unobserveDeep(observeDeepCb)
153
+ },
154
+ yjsOrigin,
155
+ }
156
+ }
@@ -0,0 +1,45 @@
1
+ import * as Y from "yjs"
2
+ import { JsonValue, JsonArray, JsonObject, JsonPrimitive } from "../jsonTypes"
3
+
4
+ function isJsonPrimitive(v: JsonValue): v is JsonPrimitive {
5
+ const t = typeof v
6
+ return t === "string" || t === "number" || t === "boolean" || v === null
7
+ }
8
+
9
+ function isJsonArray(v: JsonValue): v is JsonArray {
10
+ return Array.isArray(v)
11
+ }
12
+
13
+ function isJsonObject(v: JsonValue): v is JsonObject {
14
+ return !isJsonArray(v) && typeof v === "object"
15
+ }
16
+
17
+ export function convertJsonToYjsData(v: JsonValue) {
18
+ if (v === undefined || isJsonPrimitive(v)) {
19
+ return v
20
+ }
21
+
22
+ if (isJsonArray(v)) {
23
+ const arr = new Y.Array()
24
+ applyJsonArrayYArray(arr, v)
25
+ return arr
26
+ }
27
+
28
+ if (isJsonObject(v)) {
29
+ const map = new Y.Map()
30
+ applyJsonObjectToYMap(map, v)
31
+ return map
32
+ }
33
+
34
+ throw new Error(`unsupported value type: ${v}`)
35
+ }
36
+
37
+ export function applyJsonArrayYArray(dest: Y.Array<unknown>, source: JsonArray) {
38
+ dest.push(source.map(convertJsonToYjsData))
39
+ }
40
+
41
+ export function applyJsonObjectToYMap(dest: Y.Map<unknown>, source: JsonObject) {
42
+ Object.entries(source).forEach(([k, v]) => {
43
+ dest.set(k, convertJsonToYjsData(v))
44
+ })
45
+ }
@@ -1,85 +1,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
- }
75
-
76
- return patches
77
- }
78
-
79
- function toPlainValue(v: Y.Map<any> | Y.Array<any> | JSONValue) {
80
- if (v instanceof Y.Map || v instanceof Y.Array) {
81
- return v.toJSON() as JSONObject | JSONArray
82
- } else {
83
- return v
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
+ }
75
+
76
+ return patches
77
+ }
78
+
79
+ function toPlainValue(v: Y.Map<any> | Y.Array<any> | JsonValue) {
80
+ if (v instanceof Y.Map || v instanceof Y.Array) {
81
+ return v.toJSON() as JsonObject | JsonArray
82
+ } else {
83
+ return v
84
+ }
85
+ }
@@ -0,0 +1,11 @@
1
+ import { AnyType, createContext } from "mobx-keystone"
2
+ import * as Y from "yjs"
3
+
4
+ export interface YjsBindingContext {
5
+ yjsDoc: Y.Doc
6
+ yjsObject: Y.Map<unknown> | Y.Array<unknown>
7
+ mobxKeystoneType: AnyType
8
+ yjsOrigin: symbol
9
+ }
10
+
11
+ export const yjsBindingContext = createContext<YjsBindingContext | undefined>(undefined)
package/src/index.ts CHANGED
@@ -1,2 +1,9 @@
1
- export { MobxKeystoneYjsError } from "./utils/error"
2
- export { bindYjsToMobxKeystone } from "./binding/bindYjsToMobxKeystone"
1
+ export { bindYjsToMobxKeystone } from "./binding/bindYjsToMobxKeystone"
2
+ export {
3
+ applyJsonArrayYArray,
4
+ applyJsonObjectToYMap,
5
+ convertJsonToYjsData,
6
+ } from "./binding/convertJsonToYjsData"
7
+ export { yjsBindingContext } from "./binding/yjsBindingContext"
8
+ export type { YjsBindingContext } from "./binding/yjsBindingContext"
9
+ export { MobxKeystoneYjsError } from "./utils/error"
package/src/jsonTypes.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type JSONPrimitive = string | number | boolean | null
2
- export type JSONValue = JSONPrimitive | JSONObject | JSONArray
3
- export type JSONObject = { [key: string]: JSONValue }
4
- export interface JSONArray extends Array<JSONValue> {}
1
+ export type JsonPrimitive = string | number | boolean | null
2
+ export type JsonValue = JsonPrimitive | JsonObject | JsonArray
3
+ export type JsonObject = { [key: string]: JsonValue }
4
+ export interface JsonArray extends Array<JsonValue> {}
@@ -1,3 +0,0 @@
1
- import * as Y from "yjs";
2
- import { JSONValue, JSONPrimitive } from "../jsonTypes";
3
- export declare function toYDataType(v: JSONValue): JSONPrimitive | Y.Array<unknown> | Y.Map<unknown> | undefined;
@@ -1,41 +0,0 @@
1
- import * as Y from "yjs"
2
- import { JSONValue, JSONArray, JSONObject, JSONPrimitive } from "../jsonTypes"
3
-
4
- function isJSONPrimitive(v: JSONValue): v is JSONPrimitive {
5
- const t = typeof v
6
- return t === "string" || t === "number" || t === "boolean" || v === null
7
- }
8
-
9
- function isJSONArray(v: JSONValue): v is JSONArray {
10
- return Array.isArray(v)
11
- }
12
-
13
- function isJSONObject(v: JSONValue): v is JSONObject {
14
- return !isJSONArray(v) && typeof v === "object"
15
- }
16
-
17
- export function toYDataType(v: JSONValue) {
18
- if (isJSONPrimitive(v)) {
19
- return v
20
- } else if (isJSONArray(v)) {
21
- const arr = new Y.Array()
22
- applyJsonArray(arr, v)
23
- return arr
24
- } else if (isJSONObject(v)) {
25
- const map = new Y.Map()
26
- applyJsonObject(map, v)
27
- return map
28
- } else {
29
- return undefined
30
- }
31
- }
32
-
33
- function applyJsonArray(dest: Y.Array<unknown>, source: JSONArray) {
34
- dest.push(source.map(toYDataType))
35
- }
36
-
37
- function applyJsonObject(dest: Y.Map<unknown>, source: JSONObject) {
38
- Object.entries(source).forEach(([k, v]) => {
39
- dest.set(k, toYDataType(v))
40
- })
41
- }