mobx-keystone-yjs 1.3.0 → 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.
@@ -1,11 +1,32 @@
1
1
  import { AnyDataModel, AnyModel, AnyStandardType, ModelClass, TypeToData } from "mobx-keystone";
2
2
  import * as Y from "yjs";
3
+ /**
4
+ * Creates a bidirectional binding between a Y.js data structure and a mobx-keystone model.
5
+ */
3
6
  export declare function bindYjsToMobxKeystone<TType extends AnyStandardType | ModelClass<AnyModel> | ModelClass<AnyDataModel>>({ yjsDoc, yjsObject, mobxKeystoneType, }: {
7
+ /**
8
+ * The Y.js document.
9
+ */
4
10
  yjsDoc: Y.Doc;
5
- yjsObject: Y.Map<unknown> | Y.Array<unknown>;
11
+ /**
12
+ * The bound Y.js data structure.
13
+ */
14
+ yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text;
15
+ /**
16
+ * The mobx-keystone model type.
17
+ */
6
18
  mobxKeystoneType: TType;
7
19
  }): {
20
+ /**
21
+ * The bound mobx-keystone instance.
22
+ */
8
23
  boundObject: TypeToData<TType>;
24
+ /**
25
+ * Disposes the binding.
26
+ */
9
27
  dispose(): void;
28
+ /**
29
+ * The Y.js origin symbol used for binding transactions.
30
+ */
10
31
  yjsOrigin: symbol;
11
32
  };
@@ -1,5 +1,16 @@
1
1
  import * as Y from "yjs";
2
2
  import { JsonValue, JsonArray, JsonObject, JsonPrimitive } from "../jsonTypes";
3
- export declare function convertJsonToYjsData(v: JsonValue): JsonPrimitive | JsonObject | Y.Array<unknown> | Y.Map<unknown>;
4
- export declare function applyJsonArrayYArray(dest: Y.Array<unknown>, source: JsonArray): void;
3
+ /**
4
+ * Converts a JSON value to a Y.js data structure.
5
+ * Objects are converted to Y.Maps, arrays to Y.Arrays, primitives are untouched.
6
+ * Frozen values are a special case and they are kept as immutable plain values.
7
+ */
8
+ export declare function convertJsonToYjsData(v: JsonValue): JsonPrimitive | JsonObject | Y.Array<unknown> | Y.Text | Y.Map<unknown>;
9
+ /**
10
+ * Applies a JSON array to a Y.Array, using the convertJsonToYjsData to convert the values.
11
+ */
12
+ export declare function applyJsonArrayToYArray(dest: Y.Array<unknown>, source: JsonArray): void;
13
+ /**
14
+ * Applies a JSON object to a Y.Map, using the convertJsonToYjsData to convert the values.
15
+ */
5
16
  export declare function applyJsonObjectToYMap(dest: Y.Map<unknown>, source: JsonObject): void;
@@ -0,0 +1,3 @@
1
+ import * as Y from "yjs";
2
+ import { JsonValue } from "../jsonTypes";
3
+ export declare function convertYjsDataToJson(yjsData: Y.Array<unknown> | Y.Map<unknown> | Y.Text | unknown): JsonValue;
@@ -0,0 +1 @@
1
+ export declare function resolveYjsPath(yjsObject: unknown, path: readonly (string | number)[]): unknown;
@@ -1,9 +1,35 @@
1
1
  import { AnyType } from "mobx-keystone";
2
2
  import * as Y from "yjs";
3
+ /**
4
+ * Context with info on how a mobx-keystone model is bound to a Y.js data structure.
5
+ */
3
6
  export interface YjsBindingContext {
7
+ /**
8
+ * The Y.js document.
9
+ */
4
10
  yjsDoc: Y.Doc;
5
- yjsObject: Y.Map<unknown> | Y.Array<unknown>;
11
+ /**
12
+ * The bound Y.js data structure.
13
+ */
14
+ yjsObject: Y.Map<unknown> | Y.Array<unknown> | Y.Text;
15
+ /**
16
+ * The mobx-keystone model type.
17
+ */
6
18
  mobxKeystoneType: AnyType;
19
+ /**
20
+ * The origin symbol used for transactions.
21
+ */
7
22
  yjsOrigin: symbol;
23
+ /**
24
+ * The bound mobx-keystone instance.
25
+ */
26
+ boundObject: unknown | undefined;
27
+ /**
28
+ * Whether we are currently applying Y.js changes to the mobx-keystone model.
29
+ */
30
+ isApplyingYjsChangesToMobxKeystone: boolean;
8
31
  }
32
+ /**
33
+ * Context with info on how a mobx-keystone model is bound to a Y.js data structure.
34
+ */
9
35
  export declare const yjsBindingContext: import("mobx-keystone").Context<YjsBindingContext | undefined>;
@@ -1,5 +1,6 @@
1
+ export { YjsTextModel, yjsTextModelId } from "./binding/YjsTextModel";
1
2
  export { bindYjsToMobxKeystone } from "./binding/bindYjsToMobxKeystone";
2
- export { applyJsonArrayYArray, applyJsonObjectToYMap, convertJsonToYjsData, } from "./binding/convertJsonToYjsData";
3
+ export { applyJsonArrayToYArray, applyJsonObjectToYMap, convertJsonToYjsData, } from "./binding/convertJsonToYjsData";
3
4
  export { yjsBindingContext } from "./binding/yjsBindingContext";
4
5
  export type { YjsBindingContext } from "./binding/yjsBindingContext";
5
6
  export { MobxKeystoneYjsError } from "./utils/error";
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,87 +1,91 @@
1
- {
2
- "name": "mobx-keystone-yjs",
3
- "version": "1.3.0",
4
- "description": "Yjs bindings for mobx-keystone",
5
- "keywords": [
6
- "mobx",
7
- "mobx-keystone",
8
- "yjs",
9
- "crdt",
10
- "state management"
11
- ],
12
- "repository": {
13
- "type": "git",
14
- "url": "https://github.com/xaviergonz/mobx-keystone.git"
15
- },
16
- "bugs": {
17
- "url": "https://github.com/xaviergonz/mobx-keystone/issues"
18
- },
19
- "homepage": "https://mobx-keystone.js.org",
20
- "license": "MIT",
21
- "author": "Javier González Garcés",
22
- "source": "./src/index.ts",
23
- "exports": {
24
- "./package.json": "./package.json",
25
- ".": {
26
- "import": "./dist/mobx-keystone-yjs.esm.mjs",
27
- "require": "./dist/mobx-keystone-yjs.umd.js",
28
- "script": "./dist/mobx-keystone-yjs.umd.js",
29
- "default": "./dist/mobx-keystone-yjs.esm.mjs",
30
- "types": "./dist/types/index.d.ts"
31
- }
32
- },
33
- "esmodule": "./dist/mobx-keystone-yjs.esm.js",
34
- "module": "./dist/mobx-keystone-yjs.esm.js",
35
- "jsnext:main": "./dist/mobx-keystone-yjs.esm.js",
36
- "react-native": "./dist/mobx-keystone-yjs.umd.js",
37
- "umd:main": "./dist/mobx-keystone-yjs.umd.js",
38
- "unpkg": "./dist/mobx-keystone-yjs.umd.js",
39
- "jsdelivr": "./dist/mobx-keystone-yjs.umd.js",
40
- "main": "./dist/mobx-keystone-yjs.umd.js",
41
- "types": "./dist/types/index.d.ts",
42
- "typings": "./dist/types/index.d.ts",
43
- "sideEffects": false,
44
- "files": [
45
- "src",
46
- "dist",
47
- "LICENSE",
48
- "CHANGELOG.md",
49
- "README.md"
50
- ],
51
- "scripts": {
52
- "quick-build": "tsc",
53
- "quick-build-tests": "tsc -p test",
54
- "copy-root-files": "shx cp ../../LICENSE .",
55
- "build": "yarn quick-build && yarn copy-root-files && shx rm -rf dist && vite build && shx cp dist/mobx-keystone-yjs.esm.mjs dist/mobx-keystone-yjs.esm.js",
56
- "test": "jest",
57
- "test:ci": "yarn test -i",
58
- "test:perf": "yarn build && yarn test:perf:run",
59
- "lint": "cd ../.. && yarn eslint \"packages/mobx-keystone-yjs/src/**/*.ts\" \"packages/mobx-keystone-yjs/test/**/*.ts\""
60
- },
61
- "peerDependencies": {
62
- "mobx": "^6.0.0 || ^5.0.0 || ^4.0.0",
63
- "mobx-keystone": "^1.8.1",
64
- "yjs": "^13.0.0"
65
- },
66
- "devDependencies": {
67
- "@types/jest": "^29.5.11",
68
- "@types/node": "^20.10.6",
69
- "babel-jest": "^29.7.0",
70
- "jest": "^29.7.0",
71
- "mobx": "^6.12.0",
72
- "mobx-keystone": "workspace:packages/lib",
73
- "rollup-plugin-typescript2": "^0.36.0",
74
- "shx": "^0.3.4",
75
- "spec.ts": "^1.1.3",
76
- "ts-jest": "^29.1.1",
77
- "ts-node": "^10.9.2",
78
- "typescript": "^5.3.3",
79
- "vite": "^5.0.10"
80
- },
81
- "dependencies": {
82
- "tslib": "^2.6.2"
83
- },
84
- "directories": {
85
- "test": "test"
86
- }
87
- }
1
+ {
2
+ "name": "mobx-keystone-yjs",
3
+ "version": "1.4.0",
4
+ "description": "Yjs bindings for mobx-keystone",
5
+ "keywords": [
6
+ "mobx",
7
+ "mobx-keystone",
8
+ "yjs",
9
+ "crdt",
10
+ "state management"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/xaviergonz/mobx-keystone.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/xaviergonz/mobx-keystone/issues"
18
+ },
19
+ "homepage": "https://mobx-keystone.js.org",
20
+ "license": "MIT",
21
+ "author": "Javier González Garcés",
22
+ "source": "./src/index.ts",
23
+ "exports": {
24
+ "./package.json": "./package.json",
25
+ ".": {
26
+ "import": "./dist/mobx-keystone-yjs.esm.mjs",
27
+ "require": "./dist/mobx-keystone-yjs.umd.js",
28
+ "script": "./dist/mobx-keystone-yjs.umd.js",
29
+ "default": "./dist/mobx-keystone-yjs.esm.mjs",
30
+ "types": "./dist/types/index.d.ts"
31
+ }
32
+ },
33
+ "esmodule": "./dist/mobx-keystone-yjs.esm.js",
34
+ "module": "./dist/mobx-keystone-yjs.esm.js",
35
+ "jsnext:main": "./dist/mobx-keystone-yjs.esm.js",
36
+ "react-native": "./dist/mobx-keystone-yjs.umd.js",
37
+ "umd:main": "./dist/mobx-keystone-yjs.umd.js",
38
+ "unpkg": "./dist/mobx-keystone-yjs.umd.js",
39
+ "jsdelivr": "./dist/mobx-keystone-yjs.umd.js",
40
+ "main": "./dist/mobx-keystone-yjs.umd.js",
41
+ "types": "./dist/types/index.d.ts",
42
+ "typings": "./dist/types/index.d.ts",
43
+ "sideEffects": false,
44
+ "files": [
45
+ "src",
46
+ "dist",
47
+ "LICENSE",
48
+ "CHANGELOG.md",
49
+ "README.md"
50
+ ],
51
+ "scripts": {
52
+ "quick-build": "tsc",
53
+ "quick-build-tests": "tsc -p test",
54
+ "copy-root-files": "shx cp ../../LICENSE .",
55
+ "build": "yarn quick-build && yarn copy-root-files && shx rm -rf dist && vite build && shx cp dist/mobx-keystone-yjs.esm.mjs dist/mobx-keystone-yjs.esm.js",
56
+ "test": "jest",
57
+ "test:ci": "yarn test -i",
58
+ "test:perf": "yarn build && yarn test:perf:run",
59
+ "lint": "cd ../.. && yarn eslint \"packages/mobx-keystone-yjs/src/**/*.ts\" \"packages/mobx-keystone-yjs/test/**/*.ts\""
60
+ },
61
+ "peerDependencies": {
62
+ "mobx": "^6.0.0 || ^5.0.0 || ^4.0.0",
63
+ "mobx-keystone": "^1.9.0",
64
+ "yjs": "^13.0.0"
65
+ },
66
+ "devDependencies": {
67
+ "@babel/core": "^7.23.7",
68
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
69
+ "@babel/plugin-proposal-decorators": "^7.23.7",
70
+ "@babel/preset-env": "^7.23.7",
71
+ "@babel/preset-typescript": "^7.23.3",
72
+ "@types/jest": "^29.5.11",
73
+ "@types/node": "^20.10.6",
74
+ "babel-jest": "^29.7.0",
75
+ "jest": "^29.7.0",
76
+ "mobx-keystone": "workspace:packages/lib",
77
+ "rollup-plugin-typescript2": "^0.36.0",
78
+ "shx": "^0.3.4",
79
+ "spec.ts": "^1.1.3",
80
+ "ts-jest": "^29.1.1",
81
+ "ts-node": "^10.9.2",
82
+ "typescript": "^5.3.3",
83
+ "vite": "^5.0.11"
84
+ },
85
+ "dependencies": {
86
+ "tslib": "^2.6.2"
87
+ },
88
+ "directories": {
89
+ "test": "test"
90
+ }
91
+ }
@@ -0,0 +1,249 @@
1
+ import { IAtom, computed, createAtom, observe, reaction } from "mobx"
2
+ import {
3
+ Frozen,
4
+ Model,
5
+ frozen,
6
+ getParentToChildPath,
7
+ model,
8
+ onSnapshot,
9
+ tProp,
10
+ types,
11
+ } from "mobx-keystone"
12
+ import * as Y from "yjs"
13
+ import { failure } from "../utils/error"
14
+ import { YjsBindingContext, yjsBindingContext } from "./yjsBindingContext"
15
+ import { resolveYjsPath } from "./resolveYjsPath"
16
+
17
+ // Delta[][], since each single change is a Delta[]
18
+ // we use frozen so that we can reuse each delta change
19
+ const deltaListType = types.array(types.frozen(types.unchecked<unknown[]>()))
20
+
21
+ export const yjsTextModelId = "mobx-keystone-yjs/YjsTextModel"
22
+
23
+ /**
24
+ * A mobx-keystone model that represents a Yjs.Text object.
25
+ */
26
+ @model(yjsTextModelId)
27
+ export class YjsTextModel extends Model({
28
+ deltaList: tProp(deltaListType, () => []),
29
+ }) {
30
+ /**
31
+ * Helper function to create a YjsTextModel instance with a simple text.
32
+ */
33
+ static withText(text: string): YjsTextModel {
34
+ return new DecoratedYjsTextModel({
35
+ deltaList: [
36
+ frozen([
37
+ {
38
+ insert: text,
39
+ },
40
+ ]),
41
+ ],
42
+ })
43
+ }
44
+
45
+ /**
46
+ * The Y.js path from the bound object to the YjsTextModel instance.
47
+ */
48
+ @computed
49
+ private get _yjsObjectPath() {
50
+ const ctx = yjsBindingContext.get(this)
51
+ if (!ctx || ctx.boundObject == null) {
52
+ throw failure(
53
+ "the YjsTextModel instance must be part of a bound object before it can be accessed"
54
+ )
55
+ }
56
+
57
+ const path = getParentToChildPath(ctx.boundObject, this)
58
+ if (!path) {
59
+ throw failure("a path from the bound object to the YjsTextModel instance is not available")
60
+ }
61
+
62
+ return path
63
+ }
64
+
65
+ /**
66
+ * The Yjs.Text object present at this mobx-keystone node's path.
67
+ */
68
+ @computed
69
+ private get _yjsObjectAtPath(): unknown {
70
+ const path = this._yjsObjectPath
71
+
72
+ const ctx = yjsBindingContext.get(this)!
73
+
74
+ return resolveYjsPath(ctx.yjsObject, path)
75
+ }
76
+
77
+ /**
78
+ * The Yjs.Text object represented by this mobx-keystone node.
79
+ */
80
+ @computed
81
+ get yjsText(): Y.Text {
82
+ const yjsObject = this._yjsObjectAtPath
83
+
84
+ if (!(yjsObject instanceof Y.Text)) {
85
+ throw failure(`Y.Text was expected at path ${JSON.stringify(this._yjsObjectPath)}`)
86
+ }
87
+
88
+ return yjsObject
89
+ }
90
+
91
+ /**
92
+ * Atom that gets changed when the associated Y.js text changes.
93
+ */
94
+ yjsTextChangedAtom = createAtom("yjsTextChangedAtom")
95
+
96
+ /**
97
+ * The text value of the Yjs.Text object.
98
+ * Shortcut for `yjsText.toString()`, but computed.
99
+ */
100
+ @computed
101
+ get text(): string {
102
+ this.yjsTextChangedAtom.reportObserved()
103
+ return this.yjsText.toString()
104
+ }
105
+
106
+ protected onInit() {
107
+ const shouldReplicateToYjs = (ctx: YjsBindingContext | undefined): ctx is YjsBindingContext => {
108
+ return !!ctx && !!ctx.boundObject && !ctx.isApplyingYjsChangesToMobxKeystone
109
+ }
110
+
111
+ let reapplyDeltasToYjsText = false
112
+ const newDeltas: Frozen<unknown[]>[] = []
113
+
114
+ let disposeObserveDeltaList: (() => void) | undefined
115
+
116
+ const disposeReactionToDeltaListRefChange = reaction(
117
+ () => this.$.deltaList,
118
+ (deltaList) => {
119
+ disposeObserveDeltaList?.()
120
+ disposeObserveDeltaList = undefined
121
+
122
+ if (deltaList) {
123
+ disposeObserveDeltaList = observe(this.$.deltaList, (change) => {
124
+ if (reapplyDeltasToYjsText) {
125
+ // already gonna replace them all
126
+ return
127
+ }
128
+ if (!shouldReplicateToYjs(yjsBindingContext.get(this))) {
129
+ // yjs text is already up to date with these changes
130
+ return
131
+ }
132
+
133
+ if (
134
+ change.type === "splice" &&
135
+ change.removedCount === 0 &&
136
+ change.addedCount > 0 &&
137
+ change.index === this.deltaList.length
138
+ ) {
139
+ // optimization, just adding new ones to the end
140
+ newDeltas.push(...change.added)
141
+ } else {
142
+ // any other change, we need to reapply all deltas
143
+ reapplyDeltasToYjsText = true
144
+ }
145
+ })
146
+ }
147
+ },
148
+ { fireImmediately: true }
149
+ )
150
+
151
+ const disposeOnSnapshot = onSnapshot(this, () => {
152
+ try {
153
+ if (reapplyDeltasToYjsText) {
154
+ const ctx = yjsBindingContext.get(this)
155
+
156
+ if (shouldReplicateToYjs(ctx)) {
157
+ const { yjsText } = this
158
+
159
+ ctx.yjsDoc.transact(() => {
160
+ // didn't find a better way than this to reapply all deltas
161
+ // without having to re-create the Y.Text object
162
+ if (yjsText.length > 0) {
163
+ yjsText.delete(0, yjsText.length)
164
+ }
165
+
166
+ this.deltaList.forEach((frozenDeltas) => {
167
+ yjsText.applyDelta(frozenDeltas.data)
168
+ })
169
+ }, ctx.yjsOrigin)
170
+ }
171
+ } else if (newDeltas.length > 0) {
172
+ const ctx = yjsBindingContext.get(this)
173
+
174
+ if (shouldReplicateToYjs(ctx)) {
175
+ const { yjsText } = this
176
+
177
+ ctx.yjsDoc.transact(() => {
178
+ newDeltas.forEach((frozenDeltas) => {
179
+ yjsText.applyDelta(frozenDeltas.data)
180
+ })
181
+ }, ctx.yjsOrigin)
182
+ }
183
+ }
184
+ } finally {
185
+ reapplyDeltasToYjsText = false
186
+ newDeltas.length = 0
187
+ }
188
+ })
189
+
190
+ const diposeYjsTextChangedAtom = hookYjsTextChangedAtom(
191
+ () => this.yjsText,
192
+ this.yjsTextChangedAtom
193
+ )
194
+
195
+ return () => {
196
+ disposeOnSnapshot()
197
+ disposeReactionToDeltaListRefChange()
198
+ disposeObserveDeltaList?.()
199
+ disposeObserveDeltaList = undefined
200
+
201
+ diposeYjsTextChangedAtom()
202
+ }
203
+ }
204
+ }
205
+
206
+ // we use this trick just to avoid a babel bug that causes classes used inside classes not to be overriden
207
+ // by the decorator
208
+ const DecoratedYjsTextModel = YjsTextModel
209
+
210
+ function hookYjsTextChangedAtom(getYjsText: () => Y.Text, textChangedAtom: IAtom) {
211
+ let disposeObserveYjsText: (() => void) | undefined
212
+
213
+ const observeFn = () => {
214
+ textChangedAtom.reportChanged()
215
+ }
216
+
217
+ const disposeReactionToYTextChange = reaction(
218
+ () => {
219
+ try {
220
+ return getYjsText()
221
+ } catch {
222
+ return undefined
223
+ }
224
+ },
225
+ (yjsText) => {
226
+ disposeObserveYjsText?.()
227
+ disposeObserveYjsText = undefined
228
+
229
+ if (yjsText) {
230
+ yjsText.observe(observeFn)
231
+
232
+ disposeObserveYjsText = () => {
233
+ yjsText.unobserve(observeFn)
234
+ }
235
+ }
236
+
237
+ textChangedAtom.reportChanged()
238
+ },
239
+ {
240
+ fireImmediately: true,
241
+ }
242
+ )
243
+
244
+ return () => {
245
+ disposeReactionToYTextChange()
246
+ disposeObserveYjsText?.()
247
+ disposeObserveYjsText = undefined
248
+ }
249
+ }