@loro-extended/change 5.4.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.
@@ -74,6 +74,7 @@ export class DocRefInternals<
74
74
  autoCommit: this.getAutoCommit(),
75
75
  batchedMutation: this.getBatchedMutation(),
76
76
  getDoc: () => this.doc,
77
+ overlay: this.getOverlay(),
77
78
  }
78
79
  }
79
80
 
@@ -0,0 +1,17 @@
1
+ export type { DiffOverlay } from "./base.js"
2
+
3
+ export type { CounterRef } from "./counter-ref.js"
4
+
5
+ export type { ListRef } from "./list-ref.js"
6
+
7
+ export type { MovableListRef } from "./movable-list-ref.js"
8
+
9
+ export type { RecordRef } from "./record-ref.js"
10
+
11
+ export type { StructRef } from "./struct-ref.js"
12
+
13
+ export type { TextRef } from "./text-ref.js"
14
+
15
+ export type { TreeNodeRef } from "./tree-node-ref.js"
16
+
17
+ export type { TreeRef } from "./tree-ref.js"
@@ -185,7 +185,7 @@ describe("JSON Compatibility", () => {
185
185
 
186
186
  it("should be efficient (not access unrelated parts)", () => {
187
187
  const loroDoc = new LoroDoc()
188
- const doc = createTypedDoc(ChatSchema, loroDoc)
188
+ const doc = createTypedDoc(ChatSchema, { doc: loroDoc })
189
189
 
190
190
  change(doc, (root: any) => {
191
191
  root.messages.push({ id: "1", content: "A", timestamp: 1 })
@@ -1,5 +1,7 @@
1
1
  import type {
2
2
  Container,
3
+ Delta,
4
+ ListDiff,
3
5
  LoroDoc,
4
6
  LoroEventBatch,
5
7
  LoroList,
@@ -38,6 +40,39 @@ export class ListRefBaseInternals<
38
40
  MutableItem = NestedShape["_mutable"],
39
41
  > extends BaseRefInternals<any> {
40
42
  private itemCache = new Map<number, any>()
43
+ private overlayListCache?: Item[]
44
+
45
+ getOverlayList(): Item[] | undefined {
46
+ const overlay = this.getOverlay()
47
+ if (!overlay) {
48
+ return undefined
49
+ }
50
+
51
+ const shape = this.getShape()
52
+ if (!isValueShape(shape.shape)) {
53
+ return undefined
54
+ }
55
+
56
+ if (!this.overlayListCache) {
57
+ const container = this.getContainer() as LoroList | LoroMovableList
58
+ const diff = overlay.get(container.id)
59
+ if (!diff || diff.type !== "list") {
60
+ return undefined
61
+ }
62
+
63
+ const afterValues: Item[] = []
64
+ for (let i = 0; i < container.length; i++) {
65
+ afterValues.push(container.get(i) as Item)
66
+ }
67
+
68
+ this.overlayListCache = applyListDelta(
69
+ afterValues,
70
+ (diff as ListDiff).diff as Delta<Item[]>[],
71
+ )
72
+ }
73
+
74
+ return this.overlayListCache
75
+ }
41
76
 
42
77
  /** Get typed ref params for creating child refs at an index */
43
78
  getChildTypedRefParams(
@@ -58,6 +93,7 @@ export class ListRefBaseInternals<
58
93
  autoCommit: this.getAutoCommit(),
59
94
  batchedMutation: this.getBatchedMutation(),
60
95
  getDoc: () => this.getDoc(),
96
+ overlay: this.getOverlay(),
61
97
  }
62
98
  }
63
99
 
@@ -65,6 +101,7 @@ export class ListRefBaseInternals<
65
101
  getPredicateItem(index: number): Item | undefined {
66
102
  const shape = this.getShape()
67
103
  const container = this.getContainer() as LoroList | LoroMovableList
104
+ const overlayList = this.getOverlayList()
68
105
 
69
106
  // CRITICAL FIX: For predicates to work correctly with mutations,
70
107
  // we need to check if there's a cached (mutated) version first
@@ -74,6 +111,10 @@ export class ListRefBaseInternals<
74
111
  return cachedItem as Item
75
112
  }
76
113
 
114
+ if (overlayList && isValueShape(shape.shape)) {
115
+ return overlayList[index]
116
+ }
117
+
77
118
  const containerItem = container.get(index)
78
119
  if (containerItem === undefined) {
79
120
  return undefined as Item
@@ -114,9 +155,12 @@ export class ListRefBaseInternals<
114
155
  getMutableItem(index: number): MutableItem | undefined {
115
156
  const shape = this.getShape()
116
157
  const container = this.getContainer() as LoroList | LoroMovableList
158
+ const overlayList = this.getOverlayList()
117
159
 
118
160
  // Get the raw container item
119
- const containerItem = container.get(index)
161
+ const containerItem = overlayList
162
+ ? (overlayList[index] as Item | undefined)
163
+ : (container.get(index) as Item | undefined)
120
164
  if (containerItem === undefined) {
121
165
  return undefined as MutableItem
122
166
  }
@@ -462,6 +506,10 @@ export abstract class ListRefBase<
462
506
  }
463
507
 
464
508
  toArray(): Item[] {
509
+ const overlayList = this[INTERNAL_SYMBOL].getOverlayList()
510
+ if (overlayList) {
511
+ return [...overlayList]
512
+ }
465
513
  const result: Item[] = []
466
514
  for (let i = 0; i < this.length; i++) {
467
515
  result.push(this[INTERNAL_SYMBOL].getPredicateItem(i) as Item)
@@ -471,10 +519,11 @@ export abstract class ListRefBase<
471
519
 
472
520
  toJSON(): Item[] {
473
521
  const shape = this[INTERNAL_SYMBOL].getShape()
522
+ const overlayList = this[INTERNAL_SYMBOL].getOverlayList()
474
523
  const container = this[INTERNAL_SYMBOL].getContainer() as
475
524
  | LoroList
476
525
  | LoroMovableList
477
- const nativeJson = container.toJSON() as any[]
526
+ const nativeJson = overlayList ?? (container.toJSON() as any[])
478
527
 
479
528
  // If the nested shape is a container shape (map, record, etc.) or an object value shape,
480
529
  // we need to overlay placeholders for each item
@@ -511,9 +560,35 @@ export abstract class ListRefBase<
511
560
  }
512
561
 
513
562
  get length(): number {
563
+ const overlayList = this[INTERNAL_SYMBOL].getOverlayList()
564
+ if (overlayList) {
565
+ return overlayList.length
566
+ }
514
567
  const container = this[INTERNAL_SYMBOL].getContainer() as
515
568
  | LoroList
516
569
  | LoroMovableList
517
570
  return container.length
518
571
  }
519
572
  }
573
+
574
+ function applyListDelta<T>(input: T[], delta: Delta<T[]>[]): T[] {
575
+ const result: T[] = []
576
+ let index = 0
577
+
578
+ for (const op of delta) {
579
+ if (op.retain !== undefined) {
580
+ result.push(...input.slice(index, index + op.retain))
581
+ index += op.retain
582
+ } else if (op.delete !== undefined) {
583
+ index += op.delete
584
+ } else if (op.insert !== undefined) {
585
+ result.push(...op.insert)
586
+ }
587
+ }
588
+
589
+ if (index < input.length) {
590
+ result.push(...input.slice(index))
591
+ }
592
+
593
+ return result
594
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  LoroDoc,
4
4
  LoroEventBatch,
5
5
  LoroMap,
6
+ MapDiff,
6
7
  Subscription,
7
8
  Value,
8
9
  } from "loro-crdt"
@@ -94,6 +95,17 @@ export class RecordRefInternals<
94
95
  const container = this.getContainer() as LoroMap
95
96
 
96
97
  if (isValueShape(shape)) {
98
+ const overlay = this.getOverlay()
99
+ if (overlay) {
100
+ const containerId = (container as any).id
101
+ const diff = overlay.get(containerId)
102
+ if (diff && diff.type === "map") {
103
+ const mapDiff = diff as MapDiff
104
+ if (key in mapDiff.updated) {
105
+ return mapDiff.updated[key] as Value
106
+ }
107
+ }
108
+ }
97
109
  // When NOT in batchedMutation mode (direct access outside of change()), ALWAYS read fresh
98
110
  // from container (NEVER cache). This ensures we always get the latest value
99
111
  // from the CRDT, even when modified by a different ref instance (e.g., drafts from change())
@@ -3,6 +3,7 @@ import type {
3
3
  LoroDoc,
4
4
  LoroEventBatch,
5
5
  LoroMap,
6
+ MapDiff,
6
7
  Subscription,
7
8
  Value,
8
9
  } from "loro-crdt"
@@ -76,6 +77,17 @@ export class StructRefInternals<
76
77
  const container = this.getContainer() as LoroMap
77
78
 
78
79
  if (isValueShape(actualShape)) {
80
+ const overlay = this.getOverlay()
81
+ if (overlay) {
82
+ const containerId = (container as any).id
83
+ const diff = overlay.get(containerId)
84
+ if (diff && diff.type === "map") {
85
+ const mapDiff = diff as MapDiff
86
+ if (key in mapDiff.updated) {
87
+ return mapDiff.updated[key] as Value
88
+ }
89
+ }
90
+ }
79
91
  // When NOT in batchedMutation mode (direct access outside of change()), ALWAYS read fresh
80
92
  // from container (NEVER cache). This ensures we always get the latest value
81
93
  // from the CRDT, even when modified by a different ref instance (e.g., drafts from change())
@@ -1,4 +1,11 @@
1
- import type { LoroDoc, LoroEventBatch, LoroText, Subscription } from "loro-crdt"
1
+ import type {
2
+ Delta,
3
+ LoroDoc,
4
+ LoroEventBatch,
5
+ LoroText,
6
+ Subscription,
7
+ TextDiff,
8
+ } from "loro-crdt"
2
9
  import type { LoroTextRef } from "../loro.js"
3
10
  import type { TextContainerShape } from "../shape.js"
4
11
  import { BaseRefInternals } from "./base.js"
@@ -55,6 +62,14 @@ export class TextRefInternals extends BaseRefInternals<TextContainerShape> {
55
62
  /** Get the text as a string */
56
63
  getStringValue(): string {
57
64
  const container = this.getContainer() as LoroText
65
+ const overlay = this.getOverlay()
66
+ if (overlay) {
67
+ const diff = overlay.get(container.id)
68
+ if (diff && diff.type === "text") {
69
+ const containerValue = container.toString()
70
+ return applyTextDelta(containerValue, (diff as TextDiff).diff)
71
+ }
72
+ }
58
73
  const containerValue = container.toString()
59
74
  if (containerValue !== "" || this.materialized) {
60
75
  return containerValue
@@ -69,12 +84,30 @@ export class TextRefInternals extends BaseRefInternals<TextContainerShape> {
69
84
 
70
85
  /** Get the text as a delta */
71
86
  toDelta(): any[] {
72
- return (this.getContainer() as LoroText).toDelta()
87
+ const container = this.getContainer() as LoroText
88
+ const overlay = this.getOverlay()
89
+ if (overlay) {
90
+ const diff = overlay.get(container.id)
91
+ if (diff && diff.type === "text") {
92
+ const base = container.toDelta() as Delta<string>[]
93
+ return applyDeltaToDelta(base, (diff as TextDiff).diff)
94
+ }
95
+ }
96
+ return container.toDelta()
73
97
  }
74
98
 
75
99
  /** Get the length of the text */
76
100
  getLength(): number {
77
- return (this.getContainer() as LoroText).length
101
+ const container = this.getContainer() as LoroText
102
+ const overlay = this.getOverlay()
103
+ if (overlay) {
104
+ const diff = overlay.get(container.id)
105
+ if (diff && diff.type === "text") {
106
+ return applyTextDelta(container.toString(), (diff as TextDiff).diff)
107
+ .length
108
+ }
109
+ }
110
+ return container.length
78
111
  }
79
112
 
80
113
  /** No plain values in text */
@@ -98,3 +131,36 @@ export class TextRefInternals extends BaseRefInternals<TextContainerShape> {
98
131
  }
99
132
  }
100
133
  }
134
+
135
+ function applyTextDelta(text: string, delta: Delta<string>[]): string {
136
+ let result = ""
137
+ let index = 0
138
+
139
+ for (const op of delta) {
140
+ if (op.retain !== undefined) {
141
+ result += text.slice(index, index + op.retain)
142
+ index += op.retain
143
+ } else if (op.delete !== undefined) {
144
+ index += op.delete
145
+ } else if (op.insert !== undefined) {
146
+ result += op.insert
147
+ }
148
+ }
149
+
150
+ if (index < text.length) {
151
+ result += text.slice(index)
152
+ }
153
+
154
+ return result
155
+ }
156
+
157
+ function applyDeltaToDelta(
158
+ base: Delta<string>[],
159
+ diff: Delta<string>[],
160
+ ): Delta<string>[] {
161
+ const baseText = base
162
+ .map(op => (op.insert !== undefined ? op.insert : ""))
163
+ .join("")
164
+ const nextText = applyTextDelta(baseText, diff)
165
+ return nextText ? [{ insert: nextText }] : []
166
+ }