@loro-extended/change 0.8.1 → 0.9.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.
Files changed (35) hide show
  1. package/README.md +78 -0
  2. package/dist/index.d.ts +190 -39
  3. package/dist/index.js +480 -295
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/change.test.ts +277 -1
  7. package/src/discriminated-union-assignability.test.ts +45 -0
  8. package/src/discriminated-union-tojson.test.ts +128 -0
  9. package/src/index.ts +7 -0
  10. package/src/placeholder-proxy.test.ts +52 -0
  11. package/src/placeholder-proxy.ts +37 -0
  12. package/src/presence-interface.ts +52 -0
  13. package/src/shape.ts +44 -50
  14. package/src/typed-doc.ts +4 -4
  15. package/src/typed-presence.ts +96 -0
  16. package/src/{draft-nodes → typed-refs}/base.ts +4 -4
  17. package/src/{draft-nodes → typed-refs}/counter.test.ts +1 -1
  18. package/src/{draft-nodes → typed-refs}/counter.ts +9 -3
  19. package/src/{draft-nodes → typed-refs}/doc.ts +27 -13
  20. package/src/typed-refs/json-compatibility.test.ts +255 -0
  21. package/src/{draft-nodes → typed-refs}/list-base.ts +79 -30
  22. package/src/{draft-nodes → typed-refs}/list.test.ts +1 -1
  23. package/src/{draft-nodes → typed-refs}/list.ts +4 -4
  24. package/src/{draft-nodes → typed-refs}/map.ts +33 -22
  25. package/src/{draft-nodes → typed-refs}/movable-list.test.ts +1 -1
  26. package/src/{draft-nodes → typed-refs}/movable-list.ts +6 -6
  27. package/src/{draft-nodes → typed-refs}/proxy-handlers.ts +25 -26
  28. package/src/{draft-nodes → typed-refs}/record.test.ts +69 -0
  29. package/src/{draft-nodes → typed-refs}/record.ts +50 -21
  30. package/src/{draft-nodes → typed-refs}/text.ts +13 -3
  31. package/src/{draft-nodes → typed-refs}/tree.ts +6 -3
  32. package/src/{draft-nodes → typed-refs}/utils.ts +23 -27
  33. package/src/types.test.ts +97 -2
  34. package/src/types.ts +62 -5
  35. package/src/draft-nodes/counter.md +0 -31
@@ -0,0 +1,255 @@
1
+ import { LoroDoc } from "loro-crdt"
2
+ import { describe, expect, it, vi } from "vitest"
3
+ import { Shape } from "../shape.js"
4
+ import { createTypedDoc, TypedDoc } from "../typed-doc.js"
5
+
6
+ const MessageSchema = Shape.map({
7
+ id: Shape.plain.string(),
8
+ content: Shape.text(),
9
+ timestamp: Shape.plain.number(),
10
+ })
11
+
12
+ const ChatSchema = Shape.doc({
13
+ messages: Shape.list(MessageSchema),
14
+ meta: Shape.map({
15
+ title: Shape.plain.string(),
16
+ count: Shape.counter(),
17
+ }),
18
+ tags: Shape.movableList(Shape.plain.string()),
19
+ settings: Shape.record(Shape.plain.boolean()),
20
+ // Tree support might be limited in current implementation
21
+ // tree: Shape.tree(Shape.map({ val: Shape.plain.number() }))
22
+ })
23
+
24
+ describe("JSON Compatibility", () => {
25
+ it("should support JSON.stringify on the whole doc", () => {
26
+ const doc = createTypedDoc(ChatSchema)
27
+
28
+ doc.change((root: any) => {
29
+ root.meta.title = "My Chat"
30
+ root.meta.count.increment(5)
31
+
32
+ root.messages.push({
33
+ id: "1",
34
+ content: "Hello",
35
+ timestamp: 123,
36
+ })
37
+
38
+ root.tags.push("work")
39
+ root.tags.push("important")
40
+
41
+ root.settings.set("notifications", true)
42
+ root.settings.set("sound", false)
43
+ })
44
+
45
+ const json = JSON.stringify(doc.value)
46
+ const parsed = JSON.parse(json)
47
+
48
+ expect(parsed).toEqual({
49
+ messages: [
50
+ {
51
+ id: "1",
52
+ content: "Hello",
53
+ timestamp: 123,
54
+ },
55
+ ],
56
+ meta: {
57
+ title: "My Chat",
58
+ count: 5,
59
+ },
60
+ tags: ["work", "important"],
61
+ settings: {
62
+ notifications: true,
63
+ sound: false,
64
+ },
65
+ })
66
+ })
67
+
68
+ it("should support Object.keys and Object.entries", () => {
69
+ const doc = createTypedDoc(ChatSchema)
70
+ doc.change((root: any) => {
71
+ root.meta.title = "Test"
72
+ })
73
+
74
+ const keys = Object.keys(doc.value)
75
+ expect(keys).toContain("messages")
76
+ expect(keys).toContain("meta")
77
+ expect(keys).toContain("tags")
78
+ expect(keys).toContain("settings")
79
+
80
+ const entries = Object.entries(doc.value.meta)
81
+ const entryMap = new Map(entries)
82
+ expect(entryMap.get("title")).toBe("Test")
83
+ expect(entryMap.get("count")).toBeDefined()
84
+ })
85
+
86
+ it("should support JSON.stringify on nested structures", () => {
87
+ const doc = createTypedDoc(ChatSchema)
88
+ doc.change((root: any) => {
89
+ root.messages.push({ id: "1", content: "A", timestamp: 1 })
90
+ })
91
+
92
+ const messagesJson = JSON.stringify(doc.value.messages)
93
+ expect(JSON.parse(messagesJson)).toEqual([
94
+ { id: "1", content: "A", timestamp: 1 },
95
+ ])
96
+ })
97
+
98
+ it("should support MovableList", () => {
99
+ const doc = createTypedDoc(ChatSchema)
100
+ doc.change((root: any) => {
101
+ root.tags.push("a")
102
+ root.tags.push("b")
103
+ root.tags.move(0, 1) // move 'a' to index 1
104
+ })
105
+
106
+ // After move: ["b", "a"]
107
+ const json = JSON.stringify(doc.value.tags)
108
+ expect(JSON.parse(json)).toEqual(["b", "a"])
109
+ })
110
+
111
+ it("should support Record", () => {
112
+ const doc = createTypedDoc(ChatSchema)
113
+ doc.change((root: any) => {
114
+ root.settings.set("dark_mode", true)
115
+ })
116
+
117
+ const json = JSON.stringify(doc.value.settings)
118
+ expect(JSON.parse(json)).toEqual({ dark_mode: true })
119
+ })
120
+
121
+ it("should support Readonly vs Mutable consistency", () => {
122
+ const doc = createTypedDoc(ChatSchema)
123
+ let mutableJson = ""
124
+
125
+ doc.change((root: any) => {
126
+ root.meta.title = "Draft"
127
+ mutableJson = JSON.stringify(root)
128
+ })
129
+
130
+ const readonlyJson = JSON.stringify(doc.value)
131
+ expect(readonlyJson).toBe(mutableJson)
132
+ })
133
+
134
+ it("should handle placeholders", () => {
135
+ const doc = createTypedDoc(ChatSchema)
136
+ // No changes made
137
+
138
+ const json = JSON.stringify(doc.value)
139
+ const parsed = JSON.parse(json)
140
+
141
+ expect(parsed.meta.title).toBe("") // Default string placeholder
142
+ expect(parsed.meta.count).toBe(0) // Default counter placeholder
143
+ expect(parsed.messages).toEqual([])
144
+ })
145
+
146
+ it("should support Array methods on Lists", () => {
147
+ const doc = createTypedDoc(ChatSchema)
148
+ doc.change((root: any) => {
149
+ root.messages.push({ id: "1", content: "A", timestamp: 10 })
150
+ root.messages.push({ id: "2", content: "B", timestamp: 20 })
151
+ })
152
+
153
+ const mapped = doc.value.messages.map(m => ({ id: m.id, txt: m.content }))
154
+ expect(JSON.stringify(mapped)).toBe(
155
+ '[{"id":"1","txt":"A"},{"id":"2","txt":"B"}]',
156
+ )
157
+
158
+ const filtered = doc.value.messages.filter(m => m.timestamp > 15)
159
+ expect(filtered).toHaveLength(1)
160
+ expect(filtered[0].id).toBe("2")
161
+ // filtered returns MutableItems (TypedRefs), so JSON.stringify should work on them
162
+ expect(JSON.stringify(filtered)).toBe(
163
+ '[{"id":"2","content":"B","timestamp":20}]',
164
+ )
165
+ })
166
+
167
+ it("should support Object.values", () => {
168
+ const doc = createTypedDoc(ChatSchema)
169
+ doc.change((root: any) => {
170
+ root.settings.set("a", true)
171
+ root.settings.set("b", false)
172
+ })
173
+
174
+ const values = Object.values(doc.value.settings)
175
+ expect(values).toContain(true)
176
+ expect(values).toContain(false)
177
+ expect(values).toHaveLength(2)
178
+ })
179
+
180
+ it("should be efficient (not access unrelated parts)", () => {
181
+ const loroDoc = new LoroDoc()
182
+ const doc = new TypedDoc(ChatSchema, loroDoc)
183
+
184
+ doc.change((root: any) => {
185
+ root.messages.push({ id: "1", content: "A", timestamp: 1 })
186
+ root.meta.title = "Test"
187
+ })
188
+
189
+ // Spy on LoroDoc methods
190
+ const getMapSpy = vi.spyOn(loroDoc, "getMap")
191
+ const getListSpy = vi.spyOn(loroDoc, "getList")
192
+
193
+ // Access messages and call toJSON
194
+ const messagesJson = doc.value.messages.toJSON()
195
+
196
+ expect(messagesJson).toHaveLength(1)
197
+
198
+ // Should have accessed "messages" list
199
+ expect(getListSpy).toHaveBeenCalledWith("messages")
200
+
201
+ // Should NOT have accessed "meta" map
202
+ expect(getMapSpy).not.toHaveBeenCalledWith("meta")
203
+
204
+ // Should NOT have accessed "settings" map (record)
205
+ expect(getMapSpy).not.toHaveBeenCalledWith("settings")
206
+ })
207
+
208
+ it("should allow calling toJSON() directly on refs", () => {
209
+ const doc = createTypedDoc(ChatSchema)
210
+ doc.change((root: any) => {
211
+ root.meta.title = "Direct"
212
+ root.meta.count.increment(10)
213
+ root.messages.push({ id: "1", content: "A", timestamp: 1 })
214
+ root.settings.set("opt", true)
215
+ })
216
+
217
+ // DocRef
218
+ expect(doc.value.toJSON()).toEqual(
219
+ expect.objectContaining({
220
+ meta: expect.objectContaining({ title: "Direct" }),
221
+ }),
222
+ )
223
+
224
+ // MapRef
225
+ expect(doc.value.meta.toJSON()).toEqual({
226
+ title: "Direct",
227
+ count: 10,
228
+ })
229
+
230
+ // ListRef
231
+ expect(doc.value.messages.toJSON()).toEqual([
232
+ { id: "1", content: "A", timestamp: 1 },
233
+ ])
234
+
235
+ // RecordRef
236
+ expect(doc.value.settings.toJSON()).toEqual({
237
+ opt: true,
238
+ })
239
+
240
+ doc.change((root: any) => {
241
+ // Inside change, these are mutable refs
242
+ expect(root.meta.toJSON()).toEqual({ title: "Direct", count: 10 })
243
+ expect(root.messages.toJSON()).toEqual([
244
+ { id: "1", content: "A", timestamp: 1 },
245
+ ])
246
+
247
+ // CounterRef
248
+ expect(root.meta.count.toJSON()).toBe(10)
249
+
250
+ // TextRef (inside message)
251
+ // root.messages[0] is a MapRef. content is TextRef.
252
+ expect(root.messages[0].content.toJSON()).toBe("A")
253
+ })
254
+ })
255
+ })
@@ -7,15 +7,15 @@ import type {
7
7
  MovableListContainerShape,
8
8
  } from "../shape.js"
9
9
  import { isContainer, isValueShape } from "../utils/type-guards.js"
10
- import { DraftNode, type DraftNodeParams } from "./base.js"
11
- import { createContainerDraftNode } from "./utils.js"
10
+ import { TypedRef, type TypedRefParams } from "./base.js"
11
+ import { createContainerTypedRef } from "./utils.js"
12
12
 
13
13
  // Shared logic for list operations
14
- export abstract class ListDraftNodeBase<
14
+ export abstract class ListRefBase<
15
15
  NestedShape extends ContainerOrValueShape,
16
16
  Item = NestedShape["_plain"],
17
- DraftItem = NestedShape["_draft"],
18
- > extends DraftNode<any> {
17
+ MutableItem = NestedShape["_mutable"],
18
+ > extends TypedRef<any> {
19
19
  // Cache for items returned by array methods to track mutations
20
20
  private itemCache = new Map<number, any>()
21
21
 
@@ -40,7 +40,7 @@ export abstract class ListDraftNodeBase<
40
40
  // For value shapes, delegate to subclass-specific absorption logic
41
41
  this.absorbValueAtIndex(index, cachedItem)
42
42
  } else {
43
- // For container shapes, the item should be a draft node that handles its own absorption
43
+ // For container shapes, the item should be a typed ref that handles its own absorption
44
44
  if (
45
45
  cachedItem &&
46
46
  typeof cachedItem === "object" &&
@@ -78,10 +78,10 @@ export abstract class ListDraftNodeBase<
78
78
  }
79
79
  }
80
80
 
81
- getDraftNodeParams(
81
+ getTypedRefParams(
82
82
  index: number,
83
83
  shape: ContainerShape,
84
- ): DraftNodeParams<ContainerShape> {
84
+ ): TypedRefParams<ContainerShape> {
85
85
  return {
86
86
  shape,
87
87
  placeholder: undefined, // List items don't have placeholder
@@ -142,8 +142,8 @@ export abstract class ListDraftNodeBase<
142
142
  }
143
143
  }
144
144
 
145
- // Get item for return values - returns DraftItem that can be mutated
146
- protected getDraftItem(index: number): any {
145
+ // Get item for return values - returns MutableItem that can be mutated
146
+ protected getMutableItem(index: number): any {
147
147
  // Check if we already have a cached item for this index
148
148
  let cachedItem = this.itemCache.get(index)
149
149
  if (cachedItem) {
@@ -153,7 +153,7 @@ export abstract class ListDraftNodeBase<
153
153
  // Get the raw container item
154
154
  const containerItem = this.container.get(index)
155
155
  if (containerItem === undefined) {
156
- return undefined as DraftItem
156
+ return undefined as MutableItem
157
157
  }
158
158
 
159
159
  if (isValueShape(this.shape.shape)) {
@@ -172,11 +172,11 @@ export abstract class ListDraftNodeBase<
172
172
  if (!this.readonly) {
173
173
  this.itemCache.set(index, cachedItem)
174
174
  }
175
- return cachedItem as DraftItem
175
+ return cachedItem as MutableItem
176
176
  } else {
177
- // For container shapes, create a proper draft node using the new pattern
178
- cachedItem = createContainerDraftNode(
179
- this.getDraftNodeParams(index, this.shape.shape as ContainerShape),
177
+ // For container shapes, create a proper typed ref using the new pattern
178
+ cachedItem = createContainerTypedRef(
179
+ this.getTypedRefParams(index, this.shape.shape as ContainerShape),
180
180
  )
181
181
  // Cache container nodes
182
182
  this.itemCache.set(index, cachedItem)
@@ -191,20 +191,20 @@ export abstract class ListDraftNodeBase<
191
191
  }
192
192
  }
193
193
 
194
- return cachedItem as DraftItem
194
+ return cachedItem as MutableItem
195
195
  }
196
196
  }
197
197
 
198
198
  // Array-like methods for better developer experience
199
- // DUAL INTERFACE: Predicates get Item (plain data), return values are DraftItem (mutable)
199
+ // DUAL INTERFACE: Predicates get Item (plain data), return values are MutableItem (mutable)
200
200
 
201
201
  find(
202
202
  predicate: (item: Item, index: number) => boolean,
203
- ): DraftItem | undefined {
203
+ ): MutableItem | undefined {
204
204
  for (let i = 0; i < this.length; i++) {
205
205
  const predicateItem = this.getPredicateItem(i)
206
206
  if (predicate(predicateItem, i)) {
207
- return this.getDraftItem(i) // Return mutable draft item
207
+ return this.getMutableItem(i) // Return mutable item
208
208
  }
209
209
  }
210
210
  return undefined
@@ -231,12 +231,12 @@ export abstract class ListDraftNodeBase<
231
231
  return result
232
232
  }
233
233
 
234
- filter(predicate: (item: Item, index: number) => boolean): DraftItem[] {
235
- const result: DraftItem[] = []
234
+ filter(predicate: (item: Item, index: number) => boolean): MutableItem[] {
235
+ const result: MutableItem[] = []
236
236
  for (let i = 0; i < this.length; i++) {
237
237
  const predicateItem = this.getPredicateItem(i)
238
238
  if (predicate(predicateItem, i)) {
239
- result.push(this.getDraftItem(i)) // Return mutable draft items
239
+ result.push(this.getMutableItem(i)) // Return mutable items
240
240
  }
241
241
  }
242
242
  return result
@@ -269,41 +269,90 @@ export abstract class ListDraftNodeBase<
269
269
  return true
270
270
  }
271
271
 
272
+ slice(start?: number, end?: number): MutableItem[] {
273
+ const len = this.length
274
+
275
+ // Normalize start index (following JavaScript Array.prototype.slice semantics)
276
+ const startIndex =
277
+ start === undefined
278
+ ? 0
279
+ : start < 0
280
+ ? Math.max(len + start, 0)
281
+ : Math.min(start, len)
282
+
283
+ // Normalize end index
284
+ const endIndex =
285
+ end === undefined
286
+ ? len
287
+ : end < 0
288
+ ? Math.max(len + end, 0)
289
+ : Math.min(end, len)
290
+
291
+ const result: MutableItem[] = []
292
+ for (let i = startIndex; i < endIndex; i++) {
293
+ result.push(this.getMutableItem(i))
294
+ }
295
+ return result
296
+ }
297
+
272
298
  insert(index: number, item: Item): void {
273
- if (this.readonly) throw new Error("Cannot modify readonly doc")
299
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
274
300
  // Update cache indices before performing the insert operation
275
301
  this.updateCacheForInsert(index)
276
302
  this.insertWithConversion(index, item)
277
303
  }
278
304
 
279
305
  delete(index: number, len: number): void {
280
- if (this.readonly) throw new Error("Cannot modify readonly doc")
306
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
281
307
  // Update cache indices before performing the delete operation
282
308
  this.updateCacheForDelete(index, len)
283
309
  this.container.delete(index, len)
284
310
  }
285
311
 
286
312
  push(item: Item): void {
287
- if (this.readonly) throw new Error("Cannot modify readonly doc")
313
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
288
314
  this.pushWithConversion(item)
289
315
  }
290
316
 
291
317
  pushContainer(container: Container): Container {
292
- if (this.readonly) throw new Error("Cannot modify readonly doc")
318
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
293
319
  return this.container.pushContainer(container)
294
320
  }
295
321
 
296
322
  insertContainer(index: number, container: Container): Container {
297
- if (this.readonly) throw new Error("Cannot modify readonly doc")
323
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
298
324
  return this.container.insertContainer(index, container)
299
325
  }
300
326
 
301
- get(index: number): DraftItem {
302
- return this.getDraftItem(index)
327
+ get(index: number): MutableItem {
328
+ return this.getMutableItem(index)
303
329
  }
304
330
 
305
331
  toArray(): Item[] {
306
- return this.container.toArray() as Item[]
332
+ const result: Item[] = []
333
+ for (let i = 0; i < this.length; i++) {
334
+ result.push(this.getPredicateItem(i))
335
+ }
336
+ return result
337
+ }
338
+
339
+ toJSON(): Item[] {
340
+ return this.toArray()
341
+ }
342
+
343
+ [Symbol.iterator](): IterableIterator<MutableItem> {
344
+ let index = 0
345
+ return {
346
+ next: (): IteratorResult<MutableItem> => {
347
+ if (index < this.length) {
348
+ return { value: this.getMutableItem(index++), done: false }
349
+ }
350
+ return { value: undefined, done: true }
351
+ },
352
+ [Symbol.iterator]() {
353
+ return this
354
+ },
355
+ }
307
356
  }
308
357
 
309
358
  get length(): number {
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import { createTypedDoc, Shape } from "../index.js"
3
3
 
4
- describe("ListDraftNode", () => {
4
+ describe("ListRef", () => {
5
5
  describe("set via index", () => {
6
6
  it("should allow setting a plain object for a list item via index", () => {
7
7
  const schema = Shape.doc({
@@ -1,12 +1,12 @@
1
1
  import type { LoroList } from "loro-crdt"
2
2
  import type { ContainerOrValueShape } from "../shape.js"
3
3
  import type { Infer } from "../types.js"
4
- import { ListDraftNodeBase } from "./list-base.js"
4
+ import { ListRefBase } from "./list-base.js"
5
5
 
6
- // List draft node
7
- export class ListDraftNode<
6
+ // List typed ref
7
+ export class ListRef<
8
8
  NestedShape extends ContainerOrValueShape,
9
- > extends ListDraftNodeBase<NestedShape> {
9
+ > extends ListRefBase<NestedShape> {
10
10
  [index: number]: Infer<NestedShape>
11
11
 
12
12
  protected get container(): LoroList {
@@ -15,11 +15,8 @@ import type {
15
15
  ValueShape,
16
16
  } from "../shape.js"
17
17
  import { isContainerShape, isValueShape } from "../utils/type-guards.js"
18
- import { DraftNode, type DraftNodeParams } from "./base.js"
19
- import {
20
- assignPlainValueToDraftNode,
21
- createContainerDraftNode,
22
- } from "./utils.js"
18
+ import { TypedRef, type TypedRefParams } from "./base.js"
19
+ import { assignPlainValueToTypedRef, createContainerTypedRef } from "./utils.js"
23
20
 
24
21
  const containerConstructor = {
25
22
  counter: LoroCounter,
@@ -31,13 +28,13 @@ const containerConstructor = {
31
28
  tree: LoroTree,
32
29
  } as const
33
30
 
34
- // Map draft node
35
- export class MapDraftNode<
31
+ // Map typed ref
32
+ export class MapRef<
36
33
  NestedShapes extends Record<string, ContainerOrValueShape>,
37
- > extends DraftNode<any> {
38
- private propertyCache = new Map<string, DraftNode<ContainerShape> | Value>()
34
+ > extends TypedRef<any> {
35
+ private propertyCache = new Map<string, TypedRef<ContainerShape> | Value>()
39
36
 
40
- constructor(params: DraftNodeParams<MapContainerShape<NestedShapes>>) {
37
+ constructor(params: TypedRefParams<MapContainerShape<NestedShapes>>) {
41
38
  super(params)
42
39
  this.createLazyProperties()
43
40
  }
@@ -52,8 +49,8 @@ export class MapDraftNode<
52
49
 
53
50
  absorbPlainValues() {
54
51
  for (const [key, node] of this.propertyCache.entries()) {
55
- if (node instanceof DraftNode) {
56
- // Contains a DraftNode, not a plain Value: keep recursing
52
+ if (node instanceof TypedRef) {
53
+ // Contains a TypedRef, not a plain Value: keep recursing
57
54
  node.absorbPlainValues()
58
55
  continue
59
56
  }
@@ -63,10 +60,10 @@ export class MapDraftNode<
63
60
  }
64
61
  }
65
62
 
66
- getDraftNodeParams<S extends ContainerShape>(
63
+ getTypedRefParams<S extends ContainerShape>(
67
64
  key: string,
68
65
  shape: S,
69
- ): DraftNodeParams<ContainerShape> {
66
+ ): TypedRefParams<ContainerShape> {
70
67
  const placeholder = (this.placeholder as any)?.[key]
71
68
 
72
69
  const LoroContainer = containerConstructor[shape._type]
@@ -87,7 +84,7 @@ export class MapDraftNode<
87
84
  let node = this.propertyCache.get(key)
88
85
  if (!node) {
89
86
  if (isContainerShape(shape)) {
90
- node = createContainerDraftNode(this.getDraftNodeParams(key, shape))
87
+ node = createContainerTypedRef(this.getTypedRefParams(key, shape))
91
88
  // We cache container nodes even in readonly mode because they are just handles
92
89
  this.propertyCache.set(key, node)
93
90
  } else {
@@ -129,7 +126,7 @@ export class MapDraftNode<
129
126
  }
130
127
  }
131
128
 
132
- return node as Shape extends ContainerShape ? DraftNode<Shape> : Value
129
+ return node as Shape extends ContainerShape ? TypedRef<Shape> : Value
133
130
  }
134
131
 
135
132
  private createLazyProperties(): void {
@@ -138,7 +135,7 @@ export class MapDraftNode<
138
135
  Object.defineProperty(this, key, {
139
136
  get: () => this.getOrCreateNode(key, shape),
140
137
  set: value => {
141
- if (this.readonly) throw new Error("Cannot modify readonly doc")
138
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
142
139
  if (isValueShape(shape)) {
143
140
  this.container.set(key, value)
144
141
  this.propertyCache.set(key, value)
@@ -146,36 +143,50 @@ export class MapDraftNode<
146
143
  if (value && typeof value === "object") {
147
144
  const node = this.getOrCreateNode(key, shape)
148
145
 
149
- if (assignPlainValueToDraftNode(node as DraftNode<any>, value)) {
146
+ if (assignPlainValueToTypedRef(node as TypedRef<any>, value)) {
150
147
  return
151
148
  }
152
149
  }
153
150
  throw new Error(
154
- "Cannot set container directly, modify the draft node instead",
151
+ "Cannot set container directly, modify the typed ref instead",
155
152
  )
156
153
  }
157
154
  },
155
+ enumerable: true,
158
156
  })
159
157
  }
160
158
  }
161
159
 
160
+ toJSON(): any {
161
+ const result: any = {}
162
+ for (const key in this.shape.shapes) {
163
+ const value = (this as any)[key]
164
+ if (value && typeof value === "object" && "toJSON" in value) {
165
+ result[key] = value.toJSON()
166
+ } else {
167
+ result[key] = value
168
+ }
169
+ }
170
+ return result
171
+ }
172
+
162
173
  // TOOD(duane): return correct type here
163
174
  get(key: string): any {
164
175
  return this.container.get(key)
165
176
  }
166
177
 
167
178
  set(key: string, value: Value): void {
168
- if (this.readonly) throw new Error("Cannot modify readonly doc")
179
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
169
180
  this.container.set(key, value)
170
181
  }
171
182
 
172
183
  setContainer<C extends Container>(key: string, container: C): C {
173
- if (this.readonly) throw new Error("Cannot modify readonly doc")
184
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
174
185
  return this.container.setContainer(key, container)
175
186
  }
176
187
 
177
188
  delete(key: string): void {
178
- if (this.readonly) throw new Error("Cannot modify readonly doc")
189
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
179
190
  this.container.delete(key)
180
191
  }
181
192
 
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import { createTypedDoc, Shape } from "../index.js"
3
3
 
4
- describe("MovableListDraftNode", () => {
4
+ describe("MovableListRef", () => {
5
5
  describe("set via index", () => {
6
6
  it("should allow setting a plain object for a list item via index", () => {
7
7
  const schema = Shape.doc({
@@ -1,13 +1,13 @@
1
1
  import type { Container, LoroMovableList } from "loro-crdt"
2
2
  import type { ContainerOrValueShape } from "../shape.js"
3
3
  import type { Infer } from "../types.js"
4
- import { ListDraftNodeBase } from "./list-base.js"
4
+ import { ListRefBase } from "./list-base.js"
5
5
 
6
- // Movable list draft node
7
- export class MovableListDraftNode<
6
+ // Movable list typed ref
7
+ export class MovableListRef<
8
8
  NestedShape extends ContainerOrValueShape,
9
9
  Item = NestedShape["_plain"],
10
- > extends ListDraftNodeBase<NestedShape> {
10
+ > extends ListRefBase<NestedShape> {
11
11
  [index: number]: Infer<NestedShape>
12
12
 
13
13
  protected get container(): LoroMovableList {
@@ -20,12 +20,12 @@ export class MovableListDraftNode<
20
20
  }
21
21
 
22
22
  move(from: number, to: number): void {
23
- if (this.readonly) throw new Error("Cannot modify readonly doc")
23
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
24
24
  this.container.move(from, to)
25
25
  }
26
26
 
27
27
  set(index: number, item: Exclude<Item, Container>) {
28
- if (this.readonly) throw new Error("Cannot modify readonly doc")
28
+ if (this.readonly) throw new Error("Cannot modify readonly ref")
29
29
  return this.container.set(index, item)
30
30
  }
31
31
  }