@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.
- package/README.md +56 -36
- package/dist/index.d.ts +132 -109
- package/dist/index.js +187 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +1 -1
- 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 +33 -3
- package/src/index.ts +37 -13
- package/src/overlay-recursion.test.ts +8 -8
- package/src/shallow-fork.test.ts +1 -1
- package/src/shape.ts +7 -7
- package/src/typed-doc.ts +23 -6
- package/src/typed-refs/base.ts +16 -1
- package/src/typed-refs/counter-ref-internals.ts +9 -0
- package/src/typed-refs/doc-ref-internals.ts +1 -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.ts +77 -2
- package/src/typed-refs/record-ref-internals.ts +12 -0
- package/src/typed-refs/struct-ref-internals.ts +12 -0
- package/src/typed-refs/text-ref-internals.ts +69 -3
|
@@ -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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|