mobx-keystone-yjs 1.4.0 → 1.5.1
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 +33 -25
- package/dist/mobx-keystone-yjs.esm.js +33 -30
- package/dist/mobx-keystone-yjs.esm.mjs +33 -30
- package/dist/mobx-keystone-yjs.umd.js +33 -30
- package/dist/types/binding/bindYjsToMobxKeystone.d.ts +1 -1
- package/dist/types/binding/convertJsonToYjsData.d.ts +5 -4
- package/dist/types/binding/convertYjsDataToJson.d.ts +3 -2
- package/dist/types/binding/yjsBindingContext.d.ts +1 -1
- package/dist/types/jsonTypes.d.ts +5 -5
- package/package.json +91 -91
- package/src/binding/YjsTextModel.ts +248 -249
- package/src/binding/applyMobxKeystonePatchToYjsObject.ts +98 -96
- package/src/binding/bindYjsToMobxKeystone.ts +192 -191
- package/src/binding/convertJsonToYjsData.ts +78 -72
- package/src/binding/convertYjsDataToJson.ts +31 -31
- package/src/binding/convertYjsEventToPatches.ts +96 -92
- package/src/binding/resolveYjsPath.ts +27 -27
- package/src/binding/yjsBindingContext.ts +42 -42
- package/src/jsonTypes.ts +9 -4
- package/src/utils/getOrCreateYjsCollectionAtom.ts +27 -18
|
@@ -1,249 +1,248 @@
|
|
|
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 (
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
(
|
|
119
|
-
|
|
120
|
-
disposeObserveDeltaList
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
disposeObserveDeltaList
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
disposeObserveYjsText
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
disposeObserveYjsText
|
|
247
|
-
|
|
248
|
-
|
|
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?.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
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
104
|
+
return this.yjsText.toString()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected onInit() {
|
|
108
|
+
const shouldReplicateToYjs = (ctx: YjsBindingContext | undefined): ctx is YjsBindingContext => {
|
|
109
|
+
return !!ctx && !!ctx.boundObject && !ctx.isApplyingYjsChangesToMobxKeystone
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let reapplyDeltasToYjsText = false
|
|
113
|
+
const newDeltas: Frozen<unknown[]>[] = []
|
|
114
|
+
|
|
115
|
+
let disposeObserveDeltaList: (() => void) | undefined
|
|
116
|
+
|
|
117
|
+
const disposeReactionToDeltaListRefChange = reaction(
|
|
118
|
+
() => this.$.deltaList,
|
|
119
|
+
(deltaList) => {
|
|
120
|
+
disposeObserveDeltaList?.()
|
|
121
|
+
disposeObserveDeltaList = undefined
|
|
122
|
+
|
|
123
|
+
disposeObserveDeltaList = observe(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
|
+
{ fireImmediately: true }
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const disposeOnSnapshot = onSnapshot(this, () => {
|
|
151
|
+
try {
|
|
152
|
+
if (reapplyDeltasToYjsText) {
|
|
153
|
+
const ctx = yjsBindingContext.get(this)
|
|
154
|
+
|
|
155
|
+
if (shouldReplicateToYjs(ctx)) {
|
|
156
|
+
const { yjsText } = this
|
|
157
|
+
|
|
158
|
+
ctx.yjsDoc.transact(() => {
|
|
159
|
+
// didn't find a better way than this to reapply all deltas
|
|
160
|
+
// without having to re-create the Y.Text object
|
|
161
|
+
if (yjsText.length > 0) {
|
|
162
|
+
yjsText.delete(0, yjsText.length)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.deltaList.forEach((frozenDeltas) => {
|
|
166
|
+
yjsText.applyDelta(frozenDeltas.data)
|
|
167
|
+
})
|
|
168
|
+
}, ctx.yjsOrigin)
|
|
169
|
+
}
|
|
170
|
+
} else if (newDeltas.length > 0) {
|
|
171
|
+
const ctx = yjsBindingContext.get(this)
|
|
172
|
+
|
|
173
|
+
if (shouldReplicateToYjs(ctx)) {
|
|
174
|
+
const { yjsText } = this
|
|
175
|
+
|
|
176
|
+
ctx.yjsDoc.transact(() => {
|
|
177
|
+
newDeltas.forEach((frozenDeltas) => {
|
|
178
|
+
yjsText.applyDelta(frozenDeltas.data)
|
|
179
|
+
})
|
|
180
|
+
}, ctx.yjsOrigin)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
reapplyDeltasToYjsText = false
|
|
185
|
+
newDeltas.length = 0
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const diposeYjsTextChangedAtom = hookYjsTextChangedAtom(
|
|
190
|
+
() => this.yjsText,
|
|
191
|
+
this.yjsTextChangedAtom
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return () => {
|
|
195
|
+
disposeOnSnapshot()
|
|
196
|
+
disposeReactionToDeltaListRefChange()
|
|
197
|
+
disposeObserveDeltaList?.()
|
|
198
|
+
disposeObserveDeltaList = undefined
|
|
199
|
+
|
|
200
|
+
diposeYjsTextChangedAtom()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// we use this trick just to avoid a babel bug that causes classes used inside classes not to be overriden
|
|
206
|
+
// by the decorator
|
|
207
|
+
const DecoratedYjsTextModel = YjsTextModel
|
|
208
|
+
|
|
209
|
+
function hookYjsTextChangedAtom(getYjsText: () => Y.Text, textChangedAtom: IAtom) {
|
|
210
|
+
let disposeObserveYjsText: (() => void) | undefined
|
|
211
|
+
|
|
212
|
+
const observeFn = () => {
|
|
213
|
+
textChangedAtom.reportChanged()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const disposeReactionToYTextChange = reaction(
|
|
217
|
+
() => {
|
|
218
|
+
try {
|
|
219
|
+
return getYjsText()
|
|
220
|
+
} catch {
|
|
221
|
+
return undefined
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
(yjsText) => {
|
|
225
|
+
disposeObserveYjsText?.()
|
|
226
|
+
disposeObserveYjsText = undefined
|
|
227
|
+
|
|
228
|
+
if (yjsText) {
|
|
229
|
+
yjsText.observe(observeFn)
|
|
230
|
+
|
|
231
|
+
disposeObserveYjsText = () => {
|
|
232
|
+
yjsText.unobserve(observeFn)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
textChangedAtom.reportChanged()
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
fireImmediately: true,
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return () => {
|
|
244
|
+
disposeReactionToYTextChange()
|
|
245
|
+
disposeObserveYjsText?.()
|
|
246
|
+
disposeObserveYjsText = undefined
|
|
247
|
+
}
|
|
248
|
+
}
|