mobx-keystone-loro 1.0.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 +8 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/mobx-keystone-loro.esm.js +957 -0
- package/dist/mobx-keystone-loro.esm.mjs +957 -0
- package/dist/mobx-keystone-loro.umd.js +957 -0
- package/dist/types/binding/LoroTextModel.d.ts +72 -0
- package/dist/types/binding/applyLoroEventToMobx.d.ts +9 -0
- package/dist/types/binding/applyMobxChangeToLoroObject.d.ts +7 -0
- package/dist/types/binding/bindLoroToMobxKeystone.d.ts +33 -0
- package/dist/types/binding/convertJsonToLoroData.d.ts +51 -0
- package/dist/types/binding/convertLoroDataToJson.d.ts +11 -0
- package/dist/types/binding/loroBindingContext.d.ts +39 -0
- package/dist/types/binding/loroSnapshotTracking.d.ts +26 -0
- package/dist/types/binding/moveWithinArray.d.ts +36 -0
- package/dist/types/binding/resolveLoroPath.d.ts +11 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/plainTypes.d.ts +6 -0
- package/dist/types/utils/error.d.ts +4 -0
- package/dist/types/utils/getOrCreateLoroCollectionAtom.d.ts +7 -0
- package/dist/types/utils/isBindableLoroContainer.d.ts +9 -0
- package/package.json +92 -0
- package/src/binding/LoroTextModel.ts +211 -0
- package/src/binding/applyLoroEventToMobx.ts +280 -0
- package/src/binding/applyMobxChangeToLoroObject.ts +182 -0
- package/src/binding/bindLoroToMobxKeystone.ts +353 -0
- package/src/binding/convertJsonToLoroData.ts +315 -0
- package/src/binding/convertLoroDataToJson.ts +68 -0
- package/src/binding/loroBindingContext.ts +46 -0
- package/src/binding/loroSnapshotTracking.ts +36 -0
- package/src/binding/moveWithinArray.ts +112 -0
- package/src/binding/resolveLoroPath.ts +37 -0
- package/src/index.ts +16 -0
- package/src/plainTypes.ts +7 -0
- package/src/utils/error.ts +12 -0
- package/src/utils/getOrCreateLoroCollectionAtom.ts +17 -0
- package/src/utils/isBindableLoroContainer.ts +13 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { ContainerID, ListDiff, LoroDoc, LoroEvent, MapDiff } from "loro-crdt"
|
|
2
|
+
import { isContainer, LoroMap, LoroMovableList, LoroText } from "loro-crdt"
|
|
3
|
+
import { remove } from "mobx"
|
|
4
|
+
import {
|
|
5
|
+
Frozen,
|
|
6
|
+
fromSnapshot,
|
|
7
|
+
frozen,
|
|
8
|
+
getSnapshotModelId,
|
|
9
|
+
isDataModel,
|
|
10
|
+
isFrozenSnapshot,
|
|
11
|
+
isModel,
|
|
12
|
+
modelIdKey,
|
|
13
|
+
Path,
|
|
14
|
+
resolvePath,
|
|
15
|
+
runUnprotected,
|
|
16
|
+
} from "mobx-keystone"
|
|
17
|
+
import type { PlainValue } from "../plainTypes"
|
|
18
|
+
import { failure } from "../utils/error"
|
|
19
|
+
import { convertLoroDataToJson } from "./convertLoroDataToJson"
|
|
20
|
+
|
|
21
|
+
// Represents the map of potential objects to reconcile (ID -> Object)
|
|
22
|
+
export type ReconciliationMap = Map<string, object>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Applies a Loro event directly to the MobX model tree using proper mutations
|
|
26
|
+
* (splice for arrays, property assignment for objects).
|
|
27
|
+
* This is more efficient than converting to patches first.
|
|
28
|
+
*/
|
|
29
|
+
export function applyLoroEventToMobx(
|
|
30
|
+
event: LoroEvent,
|
|
31
|
+
loroDoc: LoroDoc,
|
|
32
|
+
boundObject: object,
|
|
33
|
+
rootPath: Path,
|
|
34
|
+
reconciliationMap: ReconciliationMap,
|
|
35
|
+
newlyInsertedContainers: Set<ContainerID>
|
|
36
|
+
): void {
|
|
37
|
+
// Skip events for containers that were just inserted as part of another container
|
|
38
|
+
// Their content is already included in the parent's convertLoroDataToJson call
|
|
39
|
+
if (newlyInsertedContainers.has(event.target)) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Resolve the path relative to the root
|
|
44
|
+
const eventPath = loroDoc.getPathToContainer(event.target)
|
|
45
|
+
if (!eventPath) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Strip the root path to get relative path
|
|
50
|
+
const relativePath = resolveEventPath(eventPath, rootPath)
|
|
51
|
+
if (relativePath === undefined) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { value: target } = resolvePath(boundObject, relativePath)
|
|
56
|
+
|
|
57
|
+
if (!target) {
|
|
58
|
+
throw failure(`cannot resolve path ${JSON.stringify(relativePath)}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Wrap in runUnprotected since we're modifying the tree from outside a model action
|
|
62
|
+
runUnprotected(() => {
|
|
63
|
+
const diff = event.diff
|
|
64
|
+
if (diff.type === "map") {
|
|
65
|
+
applyMapEventToMobx(diff, loroDoc, event.target, target, reconciliationMap)
|
|
66
|
+
} else if (diff.type === "list") {
|
|
67
|
+
applyListEventToMobx(
|
|
68
|
+
diff,
|
|
69
|
+
loroDoc,
|
|
70
|
+
event.target,
|
|
71
|
+
target,
|
|
72
|
+
reconciliationMap,
|
|
73
|
+
newlyInsertedContainers
|
|
74
|
+
)
|
|
75
|
+
} else if (diff.type === "text") {
|
|
76
|
+
applyTextEventToMobx(loroDoc, event.target, target)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function processDeletedValue(val: unknown, reconciliationMap: ReconciliationMap) {
|
|
82
|
+
// Handle both Model and DataModel instances
|
|
83
|
+
if (isModel(val) || isDataModel(val)) {
|
|
84
|
+
const id = modelIdKey in val ? val[modelIdKey] : undefined
|
|
85
|
+
if (id) {
|
|
86
|
+
reconciliationMap.set(id, val)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function reviveValue(jsonValue: unknown, reconciliationMap: ReconciliationMap): unknown {
|
|
92
|
+
// Handle primitives
|
|
93
|
+
if (jsonValue === null || typeof jsonValue !== "object") {
|
|
94
|
+
return jsonValue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle frozen
|
|
98
|
+
if (isFrozenSnapshot(jsonValue)) {
|
|
99
|
+
return frozen(jsonValue.data)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If we have a reconciliation map and the value looks like a model with an ID, check if we have it
|
|
103
|
+
if (reconciliationMap) {
|
|
104
|
+
const modelId = getSnapshotModelId(jsonValue)
|
|
105
|
+
if (modelId) {
|
|
106
|
+
const existing = reconciliationMap.get(modelId)
|
|
107
|
+
if (existing) {
|
|
108
|
+
reconciliationMap.delete(modelId)
|
|
109
|
+
return existing
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return fromSnapshot(jsonValue)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function applyMapEventToMobx(
|
|
118
|
+
diff: MapDiff,
|
|
119
|
+
loroDoc: LoroDoc,
|
|
120
|
+
containerTarget: ContainerID,
|
|
121
|
+
target: Record<string, unknown>,
|
|
122
|
+
reconciliationMap: ReconciliationMap
|
|
123
|
+
): void {
|
|
124
|
+
const container = loroDoc.getContainerById(containerTarget)
|
|
125
|
+
|
|
126
|
+
if (!container || !(container instanceof LoroMap)) {
|
|
127
|
+
throw failure(`${containerTarget} was not a Loro map`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Process additions and updates from diff.updated
|
|
131
|
+
for (const key of Object.keys(diff.updated)) {
|
|
132
|
+
const loroValue = container.get(key)
|
|
133
|
+
|
|
134
|
+
if (loroValue === undefined) {
|
|
135
|
+
// Key was deleted (Loro returns undefined for deleted keys)
|
|
136
|
+
if (key in target) {
|
|
137
|
+
processDeletedValue(target[key], reconciliationMap)
|
|
138
|
+
// Use MobX's remove to properly delete the key from the observable object
|
|
139
|
+
if (isModel(target)) {
|
|
140
|
+
remove(target.$, key)
|
|
141
|
+
} else {
|
|
142
|
+
remove(target, key)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Key was added or updated
|
|
147
|
+
if (key in target) {
|
|
148
|
+
processDeletedValue(target[key], reconciliationMap)
|
|
149
|
+
}
|
|
150
|
+
const jsonValue = convertLoroDataToJson(loroValue as PlainValue)
|
|
151
|
+
target[key] = reviveValue(jsonValue, reconciliationMap)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applyListEventToMobx(
|
|
157
|
+
diff: ListDiff,
|
|
158
|
+
loroDoc: LoroDoc,
|
|
159
|
+
containerTarget: ContainerID,
|
|
160
|
+
target: unknown[],
|
|
161
|
+
reconciliationMap: ReconciliationMap,
|
|
162
|
+
newlyInsertedContainers: Set<ContainerID>
|
|
163
|
+
): void {
|
|
164
|
+
const container = loroDoc.getContainerById(containerTarget)
|
|
165
|
+
|
|
166
|
+
if (!container || !(container instanceof LoroMovableList)) {
|
|
167
|
+
throw failure(`${containerTarget} was not a Loro movable list`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Process delta operations in order
|
|
171
|
+
let currentIndex = 0
|
|
172
|
+
|
|
173
|
+
for (const change of diff.diff) {
|
|
174
|
+
if (change.retain) {
|
|
175
|
+
currentIndex += change.retain
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (change.delete) {
|
|
179
|
+
// Capture deleted items for reconciliation
|
|
180
|
+
const deletedItems = target.slice(currentIndex, currentIndex + change.delete)
|
|
181
|
+
deletedItems.forEach((item) => {
|
|
182
|
+
processDeletedValue(item, reconciliationMap)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Delete items at current position
|
|
186
|
+
target.splice(currentIndex, change.delete)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (change.insert) {
|
|
190
|
+
// Insert items at current position
|
|
191
|
+
const insertedItems = change.insert
|
|
192
|
+
const values = insertedItems.map((loroValue) => {
|
|
193
|
+
// Track container IDs to avoid double-processing their events
|
|
194
|
+
// When a container with data is inserted, Loro fires events for both
|
|
195
|
+
// the container insert and the container's content, but the content
|
|
196
|
+
// is already included in convertLoroDataToJson
|
|
197
|
+
if (isContainer(loroValue)) {
|
|
198
|
+
newlyInsertedContainers.add(loroValue.id)
|
|
199
|
+
// Also recursively track any nested containers
|
|
200
|
+
collectNestedContainerIds(loroValue, newlyInsertedContainers)
|
|
201
|
+
}
|
|
202
|
+
const jsonValue = convertLoroDataToJson(loroValue as PlainValue)
|
|
203
|
+
return reviveValue(jsonValue, reconciliationMap)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
target.splice(currentIndex, 0, ...values)
|
|
207
|
+
currentIndex += values.length
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function applyTextEventToMobx(
|
|
213
|
+
loroDoc: LoroDoc,
|
|
214
|
+
containerTarget: ContainerID,
|
|
215
|
+
target: { deltaList?: Frozen<unknown[]> }
|
|
216
|
+
): void {
|
|
217
|
+
const container = loroDoc.getContainerById(containerTarget)
|
|
218
|
+
|
|
219
|
+
if (!container || !(container instanceof LoroText)) {
|
|
220
|
+
throw failure(`${containerTarget} was not a Loro text container`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// LoroTextModel has deltaList as a single Frozen<LoroTextDeltaList>, not an array
|
|
224
|
+
// Replace it with the current delta from the LoroText
|
|
225
|
+
if (!("deltaList" in target)) {
|
|
226
|
+
throw failure("target does not have a deltaList property - expected LoroTextModel")
|
|
227
|
+
}
|
|
228
|
+
target.deltaList = frozen(container.toDelta())
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Recursively collects all container IDs from a Loro container.
|
|
233
|
+
* This is used to track containers that have been inserted and should not
|
|
234
|
+
* have their events processed again (since their content was already included
|
|
235
|
+
* in the parent's convertLoroDataToJson call).
|
|
236
|
+
*/
|
|
237
|
+
function collectNestedContainerIds(container: unknown, containerIds: Set<ContainerID>): void {
|
|
238
|
+
if (!isContainer(container)) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add this container's ID
|
|
243
|
+
containerIds.add(container.id)
|
|
244
|
+
|
|
245
|
+
// Handle LoroMap - iterate over values
|
|
246
|
+
if (container instanceof LoroMap) {
|
|
247
|
+
for (const key of Object.keys(container.toJSON())) {
|
|
248
|
+
const value = container.get(key)
|
|
249
|
+
collectNestedContainerIds(value, containerIds)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handle LoroMovableList - iterate over items
|
|
254
|
+
if (container instanceof LoroMovableList) {
|
|
255
|
+
for (let i = 0; i < container.length; i++) {
|
|
256
|
+
collectNestedContainerIds(container.get(i), containerIds)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// LoroText doesn't contain nested containers
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolves the path from a Loro event to a mobx-keystone path.
|
|
265
|
+
* The event path is the path from the doc root to the container that emitted the event.
|
|
266
|
+
* We need to strip the path of our root container to get a path relative to our bound object.
|
|
267
|
+
*/
|
|
268
|
+
function resolveEventPath(eventPath: Path, rootPath: Path): Path | undefined {
|
|
269
|
+
if (eventPath.length < rootPath.length) {
|
|
270
|
+
return undefined
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < rootPath.length; i++) {
|
|
274
|
+
if (eventPath[i] !== rootPath[i]) {
|
|
275
|
+
return undefined
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return eventPath.slice(rootPath.length) as Path
|
|
280
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { LoroMap, LoroMovableList, LoroText } from "loro-crdt"
|
|
2
|
+
import { DeepChange, DeepChangeType } from "mobx-keystone"
|
|
3
|
+
import type { PlainValue } from "../plainTypes"
|
|
4
|
+
import { failure } from "../utils/error"
|
|
5
|
+
import type { BindableLoroContainer } from "../utils/isBindableLoroContainer"
|
|
6
|
+
import {
|
|
7
|
+
applyDeltaToLoroText,
|
|
8
|
+
convertJsonToLoroData,
|
|
9
|
+
extractTextDeltaFromSnapshot,
|
|
10
|
+
} from "./convertJsonToLoroData"
|
|
11
|
+
import { isLoroTextModelSnapshot } from "./LoroTextModel"
|
|
12
|
+
import type { ArrayMoveChange } from "./moveWithinArray"
|
|
13
|
+
import { resolveLoroPath } from "./resolveLoroPath"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a snapshot value to a Loro-compatible value.
|
|
17
|
+
* Note: All values passed here are already snapshots (captured at change time).
|
|
18
|
+
*/
|
|
19
|
+
function convertValue(v: unknown): unknown {
|
|
20
|
+
// Handle primitives directly
|
|
21
|
+
if (v === null || v === undefined || typeof v !== "object") {
|
|
22
|
+
return v
|
|
23
|
+
}
|
|
24
|
+
// Handle plain arrays - used for empty array init
|
|
25
|
+
if (Array.isArray(v) && v.length === 0) {
|
|
26
|
+
return new LoroMovableList()
|
|
27
|
+
}
|
|
28
|
+
// Handle LoroTextModel snapshot specially - we need to return it as-is
|
|
29
|
+
// so the caller can handle creating the LoroText container properly
|
|
30
|
+
if (isLoroTextModelSnapshot(v)) {
|
|
31
|
+
return v
|
|
32
|
+
}
|
|
33
|
+
// Value is already a snapshot, convert to Loro data
|
|
34
|
+
return convertJsonToLoroData(v as PlainValue)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Inserts a value into a LoroMovableList at the given index.
|
|
39
|
+
*/
|
|
40
|
+
function insertIntoList(list: LoroMovableList, index: number, value: unknown): void {
|
|
41
|
+
if (value instanceof LoroMap || value instanceof LoroMovableList || value instanceof LoroText) {
|
|
42
|
+
list.insertContainer(index, value)
|
|
43
|
+
} else if (isLoroTextModelSnapshot(value)) {
|
|
44
|
+
const attachedText = list.insertContainer(index, new LoroText())
|
|
45
|
+
const deltas = extractTextDeltaFromSnapshot((value as any).deltaList)
|
|
46
|
+
if (deltas.length > 0) {
|
|
47
|
+
applyDeltaToLoroText(attachedText, deltas)
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
list.insert(index, value)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sets a value in a LoroMap at the given key.
|
|
56
|
+
*/
|
|
57
|
+
function setInMap(map: LoroMap, key: string, value: unknown): void {
|
|
58
|
+
if (value === undefined) {
|
|
59
|
+
map.delete(key)
|
|
60
|
+
} else if (
|
|
61
|
+
value instanceof LoroMap ||
|
|
62
|
+
value instanceof LoroMovableList ||
|
|
63
|
+
value instanceof LoroText
|
|
64
|
+
) {
|
|
65
|
+
map.setContainer(key, value)
|
|
66
|
+
} else if (isLoroTextModelSnapshot(value)) {
|
|
67
|
+
const attachedText = map.setContainer(key, new LoroText())
|
|
68
|
+
const deltas = extractTextDeltaFromSnapshot((value as any).deltaList)
|
|
69
|
+
if (deltas.length > 0) {
|
|
70
|
+
applyDeltaToLoroText(attachedText, deltas)
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
map.set(key, value)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Applies a MobX DeepChange or an ArrayMoveChange to a Loro object.
|
|
79
|
+
*/
|
|
80
|
+
export function applyMobxChangeToLoroObject(
|
|
81
|
+
change: DeepChange | ArrayMoveChange,
|
|
82
|
+
loroObject: BindableLoroContainer
|
|
83
|
+
): void {
|
|
84
|
+
const loroContainer = resolveLoroPath(loroObject, change.path)
|
|
85
|
+
|
|
86
|
+
// If container doesn't exist at this path, throw an error
|
|
87
|
+
if (!loroContainer) {
|
|
88
|
+
throw failure(
|
|
89
|
+
`cannot apply change to missing Loro container at path: ${JSON.stringify(change.path)}`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
switch (change.type) {
|
|
94
|
+
case "ArrayMove": {
|
|
95
|
+
if (!(loroContainer instanceof LoroMovableList)) {
|
|
96
|
+
throw failure(`ArrayMove change requires a LoroMovableList container`)
|
|
97
|
+
}
|
|
98
|
+
loroContainer.move(change.fromIndex, change.toIndex)
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case DeepChangeType.ArraySplice: {
|
|
103
|
+
if (!(loroContainer instanceof LoroMovableList)) {
|
|
104
|
+
throw failure(`ArraySplice change requires a LoroMovableList container`)
|
|
105
|
+
}
|
|
106
|
+
if (change.removedValues.length > 0) {
|
|
107
|
+
loroContainer.delete(change.index, change.removedValues.length)
|
|
108
|
+
}
|
|
109
|
+
if (change.addedValues.length > 0) {
|
|
110
|
+
const valuesToInsert = change.addedValues.map(convertValue)
|
|
111
|
+
for (let i = 0; i < valuesToInsert.length; i++) {
|
|
112
|
+
insertIntoList(loroContainer, change.index + i, valuesToInsert[i])
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case DeepChangeType.ArrayUpdate: {
|
|
119
|
+
if (!(loroContainer instanceof LoroMovableList)) {
|
|
120
|
+
throw failure(`ArrayUpdate change requires a LoroMovableList container`)
|
|
121
|
+
}
|
|
122
|
+
const converted = convertValue(change.newValue)
|
|
123
|
+
if (
|
|
124
|
+
converted instanceof LoroMap ||
|
|
125
|
+
converted instanceof LoroMovableList ||
|
|
126
|
+
converted instanceof LoroText
|
|
127
|
+
) {
|
|
128
|
+
loroContainer.setContainer(change.index, converted)
|
|
129
|
+
} else if (isLoroTextModelSnapshot(converted)) {
|
|
130
|
+
const attachedText = loroContainer.setContainer(change.index, new LoroText())
|
|
131
|
+
const deltas = extractTextDeltaFromSnapshot((converted as any).deltaList)
|
|
132
|
+
if (deltas.length > 0) {
|
|
133
|
+
applyDeltaToLoroText(attachedText, deltas)
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
loroContainer.set(change.index, converted)
|
|
137
|
+
}
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case DeepChangeType.ObjectAdd:
|
|
142
|
+
case DeepChangeType.ObjectUpdate: {
|
|
143
|
+
if (loroContainer instanceof LoroText) {
|
|
144
|
+
// Handle changes to LoroText properties (mainly deltaList)
|
|
145
|
+
if (change.key === "deltaList") {
|
|
146
|
+
// change.newValue is already a snapshot (captured at change time)
|
|
147
|
+
const deltas = extractTextDeltaFromSnapshot(change.newValue)
|
|
148
|
+
if (loroContainer.length > 0) {
|
|
149
|
+
loroContainer.delete(0, loroContainer.length)
|
|
150
|
+
}
|
|
151
|
+
if (deltas.length > 0) {
|
|
152
|
+
applyDeltaToLoroText(loroContainer, deltas)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ignore other property changes on LoroText as they're not synced
|
|
156
|
+
} else if (loroContainer instanceof LoroMap) {
|
|
157
|
+
const converted = convertValue(change.newValue)
|
|
158
|
+
setInMap(loroContainer, change.key, converted)
|
|
159
|
+
} else {
|
|
160
|
+
throw failure(`ObjectAdd/ObjectUpdate change requires a LoroMap or LoroText container`)
|
|
161
|
+
}
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case DeepChangeType.ObjectRemove: {
|
|
166
|
+
if (loroContainer instanceof LoroText) {
|
|
167
|
+
// ignore removes on LoroText properties
|
|
168
|
+
} else if (loroContainer instanceof LoroMap) {
|
|
169
|
+
loroContainer.delete(change.key)
|
|
170
|
+
} else {
|
|
171
|
+
throw failure(`ObjectRemove change requires a LoroMap or LoroText container`)
|
|
172
|
+
}
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
default: {
|
|
177
|
+
// Exhaustive check - TypeScript will error if we miss a case
|
|
178
|
+
const _exhaustiveCheck: never = change
|
|
179
|
+
throw failure(`unsupported change type: ${(_exhaustiveCheck as any).type}`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|