@loro-extended/change 5.3.0 → 5.4.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/README.md +85 -28
- package/dist/index.d.ts +291 -107
- package/dist/index.js +587 -36
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/change.test.ts +1 -1
- package/src/conversion.ts +40 -4
- package/src/diff-overlay.test.ts +95 -0
- package/src/diff-overlay.ts +10 -0
- package/src/discriminated-union-tojson.test.ts +2 -2
- package/src/fork-at.test.ts +1 -1
- package/src/functional-helpers.test.ts +50 -1
- package/src/functional-helpers.ts +152 -8
- package/src/index.ts +46 -18
- package/src/loro.ts +2 -1
- package/src/nested-container-materialization.test.ts +336 -0
- package/src/overlay-recursion.test.ts +8 -8
- package/src/replay-diff.test.ts +389 -0
- package/src/replay-diff.ts +229 -0
- package/src/shallow-fork.test.ts +302 -0
- package/src/shape.ts +7 -7
- package/src/typed-doc-ownkeys.test.ts +116 -0
- package/src/typed-doc.ts +33 -10
- package/src/typed-refs/base.ts +40 -4
- package/src/typed-refs/counter-ref-internals.ts +16 -2
- package/src/typed-refs/doc-ref-internals.ts +1 -0
- package/src/typed-refs/doc-ref-ownkeys.test.ts +78 -0
- package/src/typed-refs/index.ts +17 -0
- package/src/typed-refs/json-compatibility.test.ts +1 -1
- package/src/typed-refs/list-ref-base-internals.ts +2 -1
- package/src/typed-refs/list-ref-base.ts +79 -3
- package/src/typed-refs/record-ref-internals.ts +116 -2
- package/src/typed-refs/record-ref.test.ts +522 -1
- package/src/typed-refs/record-ref.ts +72 -3
- package/src/typed-refs/struct-ref-internals.ts +40 -3
- package/src/typed-refs/text-ref-internals.ts +70 -4
- package/src/typed-refs/tree-node-ref-internals.ts +14 -2
- package/src/typed-refs/tree-ref-internals.ts +2 -1
- package/src/typed-refs/utils.ts +65 -8
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { convertInputToRef } from "./conversion.js"
|
|
3
|
+
import { createTypedDoc, loro, Shape } from "./index.js"
|
|
4
|
+
|
|
5
|
+
describe("Nested Container Materialization", () => {
|
|
6
|
+
it("syncs correctly between peers when struct has nested empty record", () => {
|
|
7
|
+
// Define a schema with a nested map
|
|
8
|
+
const schema = Shape.doc({
|
|
9
|
+
items: Shape.record(
|
|
10
|
+
Shape.struct({
|
|
11
|
+
id: Shape.plain.string(),
|
|
12
|
+
metadata: Shape.record(Shape.struct({ key: Shape.plain.string() })), // Nested map of structs
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// Create two separate documents (simulating two peers)
|
|
18
|
+
const clientDoc = createTypedDoc(schema)
|
|
19
|
+
const serverDoc = createTypedDoc(schema)
|
|
20
|
+
|
|
21
|
+
// Client creates an item with empty nested map
|
|
22
|
+
clientDoc.items.set("item-1", {
|
|
23
|
+
id: "item-1",
|
|
24
|
+
metadata: {}, // Empty nested map - BUG: may not materialize properly
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Sync client -> server
|
|
28
|
+
const clientSnapshot = loro(clientDoc).doc.export({ mode: "snapshot" })
|
|
29
|
+
loro(serverDoc).doc.import(clientSnapshot)
|
|
30
|
+
|
|
31
|
+
// Server writes to the nested map
|
|
32
|
+
const serverEntry = serverDoc.items.get("item-1")
|
|
33
|
+
expect(serverEntry).toBeDefined()
|
|
34
|
+
serverEntry?.metadata.set("entry-1", { key: "value" })
|
|
35
|
+
|
|
36
|
+
// Sync server -> client
|
|
37
|
+
const serverUpdate = loro(serverDoc).doc.export({
|
|
38
|
+
mode: "update",
|
|
39
|
+
from: loro(clientDoc).doc.version(),
|
|
40
|
+
})
|
|
41
|
+
loro(clientDoc).doc.import(serverUpdate)
|
|
42
|
+
|
|
43
|
+
// BUG: Client's metadata is EMPTY!
|
|
44
|
+
const clientEntry = clientDoc.items.get("item-1")
|
|
45
|
+
expect(clientEntry?.metadata.toJSON()).toEqual({
|
|
46
|
+
"entry-1": { key: "value" },
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("handles concurrent creation of nested containers", () => {
|
|
51
|
+
const schema = Shape.doc({
|
|
52
|
+
items: Shape.record(
|
|
53
|
+
Shape.struct({
|
|
54
|
+
id: Shape.plain.string(),
|
|
55
|
+
metadata: Shape.record(Shape.plain.string()),
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const doc1 = createTypedDoc(schema)
|
|
61
|
+
const doc2 = createTypedDoc(schema)
|
|
62
|
+
|
|
63
|
+
// Peer 1 creates item with empty metadata
|
|
64
|
+
doc1.items.set("item-1", {
|
|
65
|
+
id: "item-1",
|
|
66
|
+
metadata: {},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Sync 1 -> 2
|
|
70
|
+
loro(doc2).doc.import(loro(doc1).doc.export({ mode: "snapshot" }))
|
|
71
|
+
|
|
72
|
+
// Peer 1 writes to metadata
|
|
73
|
+
doc1.items.get("item-1")?.metadata.set("p1", "v1")
|
|
74
|
+
|
|
75
|
+
// Peer 2 writes to metadata (concurrently)
|
|
76
|
+
doc2.items.get("item-1")?.metadata.set("p2", "v2")
|
|
77
|
+
|
|
78
|
+
// Sync 1 -> 2
|
|
79
|
+
loro(doc2).doc.import(
|
|
80
|
+
loro(doc1).doc.export({ mode: "update", from: loro(doc2).doc.version() }),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Sync 2 -> 1
|
|
84
|
+
loro(doc1).doc.import(
|
|
85
|
+
loro(doc2).doc.export({ mode: "update", from: loro(doc1).doc.version() }),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Both should have both values
|
|
89
|
+
const json1 = doc1.items.get("item-1")?.metadata.toJSON()
|
|
90
|
+
const json2 = doc2.items.get("item-1")?.metadata.toJSON()
|
|
91
|
+
|
|
92
|
+
console.log("JSON1", json1)
|
|
93
|
+
console.log("JSON2", json2)
|
|
94
|
+
|
|
95
|
+
expect(json1).toEqual({ p1: "v1", p2: "v2" })
|
|
96
|
+
expect(json2).toEqual({ p1: "v1", p2: "v2" })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("materializes nested containers in Tree nodes", () => {
|
|
100
|
+
const schema = Shape.doc({
|
|
101
|
+
tree: Shape.tree(
|
|
102
|
+
Shape.struct({
|
|
103
|
+
id: Shape.plain.string(),
|
|
104
|
+
tags: Shape.record(Shape.plain.boolean()),
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const doc1 = createTypedDoc(schema)
|
|
110
|
+
const doc2 = createTypedDoc(schema)
|
|
111
|
+
|
|
112
|
+
// Create a node with empty tags
|
|
113
|
+
const node = doc1.tree.createNode({
|
|
114
|
+
id: "node-1",
|
|
115
|
+
tags: {},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Get the node ID for lookup
|
|
119
|
+
const nodeId = loro(node).container.id
|
|
120
|
+
|
|
121
|
+
// Sync 1 -> 2
|
|
122
|
+
loro(doc2).doc.import(loro(doc1).doc.export({ mode: "snapshot" }))
|
|
123
|
+
|
|
124
|
+
// Verify the container exists in doc1
|
|
125
|
+
const tags = node.data.tags
|
|
126
|
+
expect(tags).toBeDefined()
|
|
127
|
+
|
|
128
|
+
// Verify it's materialized by checking if we can get its container ID
|
|
129
|
+
const tagsContainer = loro(tags).container
|
|
130
|
+
expect(tagsContainer).toBeDefined()
|
|
131
|
+
|
|
132
|
+
// Now check doc2 using getNodeByID
|
|
133
|
+
const tree2 = loro(doc2.tree).container
|
|
134
|
+
const node2 = tree2.getNodeByID(nodeId)
|
|
135
|
+
|
|
136
|
+
expect(node2).toBeDefined()
|
|
137
|
+
if (node2) {
|
|
138
|
+
const data = node2.data // LoroMap
|
|
139
|
+
const tagsMap = data.get("tags") // Should be LoroMap
|
|
140
|
+
expect(tagsMap).toBeDefined()
|
|
141
|
+
// @ts-expect-error - kind() exists at runtime
|
|
142
|
+
expect(tagsMap.kind()).toBe("Map")
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("handles missing containers gracefully (schema evolution)", async () => {
|
|
147
|
+
// Simulate an old document that doesn't have a nested container
|
|
148
|
+
const { LoroDoc } = await import("loro-crdt")
|
|
149
|
+
const oldDoc = new LoroDoc()
|
|
150
|
+
const map = oldDoc.getMap("root")
|
|
151
|
+
map.set("existing", "value")
|
|
152
|
+
// "newField" is missing
|
|
153
|
+
oldDoc.commit()
|
|
154
|
+
|
|
155
|
+
// Load with new schema that expects "newField" as a container
|
|
156
|
+
const schema = Shape.doc({
|
|
157
|
+
root: Shape.struct({
|
|
158
|
+
existing: Shape.plain.string(),
|
|
159
|
+
newField: Shape.record(Shape.plain.string()),
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const doc = createTypedDoc(schema)
|
|
164
|
+
loro(doc).doc.import(oldDoc.export({ mode: "snapshot" }))
|
|
165
|
+
|
|
166
|
+
// Access missing container
|
|
167
|
+
const root = doc.root
|
|
168
|
+
// Should not crash
|
|
169
|
+
expect(root.existing).toBe("value")
|
|
170
|
+
|
|
171
|
+
// Accessing the missing container ref should work (it creates the wrapper)
|
|
172
|
+
const newField = root.newField
|
|
173
|
+
expect(newField).toBeDefined()
|
|
174
|
+
|
|
175
|
+
// But the underlying container doesn't exist yet?
|
|
176
|
+
// getOrCreateRef creates the wrapper.
|
|
177
|
+
// The wrapper's getContainer() calls getOrCreateContainer().
|
|
178
|
+
|
|
179
|
+
// If we read from it:
|
|
180
|
+
expect(newField.keys()).toEqual([])
|
|
181
|
+
|
|
182
|
+
// If we write to it:
|
|
183
|
+
newField.set("k", "v")
|
|
184
|
+
|
|
185
|
+
// It should create the container lazily (or eagerly on set)
|
|
186
|
+
expect(newField.get("k")).toBe("v")
|
|
187
|
+
|
|
188
|
+
// Verify in raw doc
|
|
189
|
+
const rawMap = loro(doc).doc.getMap("root")
|
|
190
|
+
const rawNewField = rawMap.get("newField")
|
|
191
|
+
expect(rawNewField).toBeDefined()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Task 1.2: Test struct with deeply nested empty containers
|
|
195
|
+
it("materializes all levels of deeply nested structs", () => {
|
|
196
|
+
const schema = Shape.doc({
|
|
197
|
+
root: Shape.struct({
|
|
198
|
+
level1: Shape.struct({
|
|
199
|
+
level2: Shape.struct({
|
|
200
|
+
level3: Shape.record(Shape.plain.string()),
|
|
201
|
+
}),
|
|
202
|
+
}),
|
|
203
|
+
}),
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const doc1 = createTypedDoc(schema)
|
|
207
|
+
const doc2 = createTypedDoc(schema)
|
|
208
|
+
|
|
209
|
+
// Initialize with empty nested structure - all levels should materialize
|
|
210
|
+
// Note: For structs, we need to access them to trigger materialization
|
|
211
|
+
// since the doc root is created lazily
|
|
212
|
+
const _root = doc1.root
|
|
213
|
+
const _level1 = doc1.root.level1
|
|
214
|
+
const _level2 = doc1.root.level1.level2
|
|
215
|
+
const _level3 = doc1.root.level1.level2.level3
|
|
216
|
+
|
|
217
|
+
// Sync doc1 -> doc2
|
|
218
|
+
loro(doc2).doc.import(loro(doc1).doc.export({ mode: "snapshot" }))
|
|
219
|
+
|
|
220
|
+
// Peer 2 writes to the deeply nested container
|
|
221
|
+
doc2.root.level1.level2.level3.set("deep-key", "deep-value")
|
|
222
|
+
|
|
223
|
+
// Sync doc2 -> doc1
|
|
224
|
+
loro(doc1).doc.import(
|
|
225
|
+
loro(doc2).doc.export({ mode: "update", from: loro(doc1).doc.version() }),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// Verify the deeply nested value is visible in doc1
|
|
229
|
+
expect(doc1.root.level1.level2.level3.get("deep-key")).toBe("deep-value")
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Task 1.3: Test list push with nested empty container
|
|
233
|
+
it("materializes nested containers when pushing to list", () => {
|
|
234
|
+
const schema = Shape.doc({
|
|
235
|
+
items: Shape.list(
|
|
236
|
+
Shape.struct({
|
|
237
|
+
id: Shape.plain.string(),
|
|
238
|
+
metadata: Shape.record(Shape.plain.string()),
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const doc1 = createTypedDoc(schema)
|
|
244
|
+
const doc2 = createTypedDoc(schema)
|
|
245
|
+
|
|
246
|
+
// Push an item with empty nested metadata
|
|
247
|
+
doc1.items.push({
|
|
248
|
+
id: "item-1",
|
|
249
|
+
metadata: {},
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// Sync doc1 -> doc2
|
|
253
|
+
loro(doc2).doc.import(loro(doc1).doc.export({ mode: "snapshot" }))
|
|
254
|
+
|
|
255
|
+
// Peer 2 writes to the nested metadata
|
|
256
|
+
const item = doc2.items[0]
|
|
257
|
+
expect(item).toBeDefined()
|
|
258
|
+
item?.metadata.set("key", "value")
|
|
259
|
+
|
|
260
|
+
// Sync doc2 -> doc1
|
|
261
|
+
loro(doc1).doc.import(
|
|
262
|
+
loro(doc2).doc.export({ mode: "update", from: loro(doc1).doc.version() }),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
// Verify the nested value is visible in doc1
|
|
266
|
+
expect(doc1.items[0]?.metadata.get("key")).toBe("value")
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Task 1.5: Test conversion API with nested empty container
|
|
270
|
+
it("convertInputToRef creates containers for empty nested values", async () => {
|
|
271
|
+
const loroCrdt = await import("loro-crdt")
|
|
272
|
+
|
|
273
|
+
const structShape = Shape.struct({
|
|
274
|
+
id: Shape.plain.string(),
|
|
275
|
+
nested: Shape.record(Shape.plain.string()),
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Convert a plain object with empty nested record
|
|
279
|
+
const result = convertInputToRef({ id: "test", nested: {} }, structShape)
|
|
280
|
+
|
|
281
|
+
// Result should be a LoroMap
|
|
282
|
+
expect(result).toBeInstanceOf(loroCrdt.LoroMap)
|
|
283
|
+
|
|
284
|
+
// The nested field should also be a LoroMap (not undefined or plain object)
|
|
285
|
+
const nestedContainer = (result as typeof loroCrdt.LoroMap.prototype).get(
|
|
286
|
+
"nested",
|
|
287
|
+
)
|
|
288
|
+
expect(nestedContainer).toBeDefined()
|
|
289
|
+
expect(nestedContainer).toBeInstanceOf(loroCrdt.LoroMap)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// Tree Node Full Sync Test (from test-plan)
|
|
293
|
+
it("syncs nested containers in Tree nodes between peers", () => {
|
|
294
|
+
const schema = Shape.doc({
|
|
295
|
+
tree: Shape.tree(
|
|
296
|
+
Shape.struct({
|
|
297
|
+
id: Shape.plain.string(),
|
|
298
|
+
tags: Shape.record(Shape.plain.boolean()),
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const doc1 = createTypedDoc(schema)
|
|
304
|
+
const doc2 = createTypedDoc(schema)
|
|
305
|
+
|
|
306
|
+
// Create a node with empty tags in doc1
|
|
307
|
+
const node = doc1.tree.createNode({
|
|
308
|
+
id: "node-1",
|
|
309
|
+
tags: {},
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// Get the node ID for later lookup
|
|
313
|
+
const nodeId = loro(node).container.id
|
|
314
|
+
|
|
315
|
+
// Sync doc1 -> doc2
|
|
316
|
+
loro(doc2).doc.import(loro(doc1).doc.export({ mode: "snapshot" }))
|
|
317
|
+
|
|
318
|
+
// Peer 2 writes to tags using raw Loro API (since TreeRef doesn't expose get by ID)
|
|
319
|
+
const tree2 = loro(doc2.tree).container
|
|
320
|
+
const node2Raw = tree2.getNodeByID(nodeId)
|
|
321
|
+
expect(node2Raw).toBeDefined()
|
|
322
|
+
if (node2Raw) {
|
|
323
|
+
const data2 = node2Raw.data as any
|
|
324
|
+
const tags2 = data2.get("tags") as any
|
|
325
|
+
tags2.set("important", true)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Sync doc2 -> doc1
|
|
329
|
+
loro(doc1).doc.import(
|
|
330
|
+
loro(doc2).doc.export({ mode: "update", from: loro(doc1).doc.version() }),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
// Verify the tag is visible in doc1's node
|
|
334
|
+
expect(node.data.tags.get("important")).toBe(true)
|
|
335
|
+
})
|
|
336
|
+
})
|
|
@@ -52,7 +52,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
52
52
|
userMap.set("name", "Alice")
|
|
53
53
|
// Note: 'role' is NOT set - should default to "guest"
|
|
54
54
|
|
|
55
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
55
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
56
56
|
const json = typedDoc.toJSON()
|
|
57
57
|
|
|
58
58
|
expect(json.users[0].name).toBe("Alice")
|
|
@@ -91,7 +91,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
91
91
|
empMap.set("name", "Bob")
|
|
92
92
|
// Note: 'level' and 'status' are NOT set
|
|
93
93
|
|
|
94
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
94
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
95
95
|
const json = typedDoc.toJSON()
|
|
96
96
|
|
|
97
97
|
expect(json.departments[0].name).toBe("Engineering")
|
|
@@ -121,7 +121,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
121
121
|
)
|
|
122
122
|
// Actually we need to create a text container properly
|
|
123
123
|
|
|
124
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
124
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
125
125
|
const json = typedDoc.toJSON()
|
|
126
126
|
|
|
127
127
|
// The counter should default to 100 if not set
|
|
@@ -147,7 +147,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
147
147
|
taskMap.set("title", "Important Task")
|
|
148
148
|
// Note: 'priority' and 'completed' are NOT set
|
|
149
149
|
|
|
150
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
150
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
151
151
|
const json = typedDoc.toJSON()
|
|
152
152
|
|
|
153
153
|
expect(json.tasks[0].title).toBe("Important Task")
|
|
@@ -174,7 +174,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
174
174
|
itemMap.set("name", "Widget")
|
|
175
175
|
// Note: 'count' is NOT set
|
|
176
176
|
|
|
177
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
177
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
178
178
|
|
|
179
179
|
// Access the list ref directly and call toJSON()
|
|
180
180
|
const listJson = typedDoc.items.toJSON()
|
|
@@ -213,7 +213,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
213
213
|
numbersList.insert(1, 2)
|
|
214
214
|
numbersList.insert(2, 3)
|
|
215
215
|
|
|
216
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
216
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
217
217
|
const json = typedDoc.toJSON()
|
|
218
218
|
|
|
219
219
|
expect(json.numbers).toEqual([1, 2, 3])
|
|
@@ -239,7 +239,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
239
239
|
userMap.set("name", "Charlie")
|
|
240
240
|
// Note: 'salary' is NOT set
|
|
241
241
|
|
|
242
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
242
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
243
243
|
const json = typedDoc.toJSON()
|
|
244
244
|
|
|
245
245
|
expect(json.usersByDept.engineering[0].name).toBe("Charlie")
|
|
@@ -294,7 +294,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
294
294
|
const dataMap = loroDoc.getMap("data")
|
|
295
295
|
dataMap.set("value", null)
|
|
296
296
|
|
|
297
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
297
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
298
298
|
const json = typedDoc.toJSON()
|
|
299
299
|
|
|
300
300
|
expect(json.data.value).toBeNull()
|