@loro-extended/change 4.0.0 → 5.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/README.md +173 -149
- package/dist/index.d.ts +962 -335
- package/dist/index.js +1040 -598
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/change.test.ts +51 -52
- package/src/functional-helpers.test.ts +316 -4
- package/src/functional-helpers.ts +96 -6
- package/src/grand-unified-api.test.ts +35 -29
- package/src/index.ts +25 -1
- package/src/json-patch.test.ts +46 -27
- package/src/loro.test.ts +449 -0
- package/src/loro.ts +273 -0
- package/src/overlay-recursion.test.ts +1 -1
- package/src/path-evaluator.ts +1 -1
- package/src/path-selector.test.ts +94 -1
- package/src/shape.ts +47 -15
- package/src/typed-doc.ts +99 -98
- package/src/typed-refs/base.ts +126 -35
- package/src/typed-refs/counter-ref-internals.ts +62 -0
- package/src/typed-refs/{counter.test.ts → counter-ref.test.ts} +5 -4
- package/src/typed-refs/counter-ref.ts +45 -0
- package/src/typed-refs/{doc.ts → doc-ref-internals.ts} +33 -38
- package/src/typed-refs/doc-ref.ts +47 -0
- package/src/typed-refs/encapsulation.test.ts +226 -0
- package/src/typed-refs/list-ref-base-internals.ts +280 -0
- package/src/typed-refs/{list-base.ts → list-ref-base.ts} +255 -160
- package/src/typed-refs/list-ref-internals.ts +21 -0
- package/src/typed-refs/{list.ts → list-ref.ts} +10 -11
- package/src/typed-refs/movable-list-ref-internals.ts +38 -0
- package/src/typed-refs/movable-list-ref.ts +31 -0
- package/src/typed-refs/proxy-handlers.ts +13 -4
- package/src/typed-refs/{record.ts → record-ref-internals.ts} +78 -79
- package/src/typed-refs/{record.test.ts → record-ref.test.ts} +21 -16
- package/src/typed-refs/record-ref.ts +80 -0
- package/src/typed-refs/struct-ref-internals.ts +195 -0
- package/src/typed-refs/{struct-value-updates.test.ts → struct-ref.test.ts} +5 -3
- package/src/typed-refs/struct-ref.ts +257 -0
- package/src/typed-refs/text-ref-internals.ts +100 -0
- package/src/typed-refs/text-ref.ts +72 -0
- package/src/typed-refs/tree-node-ref-internals.ts +111 -0
- package/src/typed-refs/{tree-node.ts → tree-node-ref.ts} +58 -94
- package/src/typed-refs/tree-ref-internals.ts +110 -0
- package/src/typed-refs/tree-ref.ts +194 -0
- package/src/typed-refs/utils.ts +21 -23
- package/src/typed-refs/counter.ts +0 -62
- package/src/typed-refs/movable-list.ts +0 -32
- package/src/typed-refs/struct.ts +0 -201
- package/src/typed-refs/text.ts +0 -91
- package/src/typed-refs/tree.ts +0 -268
- /package/src/typed-refs/{list-value-updates.test.ts → list-ref-value-updates.test.ts} +0 -0
- /package/src/typed-refs/{list.test.ts → list-ref.test.ts} +0 -0
- /package/src/typed-refs/{movable-list.test.ts → movable-list-ref.test.ts} +0 -0
- /package/src/typed-refs/{record-value-updates.test.ts → record-ref-value-updates.test.ts} +0 -0
- /package/src/typed-refs/{tree-node-value-updates.test.ts → tree-node-ref.test.ts} +0 -0
- /package/src/typed-refs/{tree.test.ts → tree-node.test.ts} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Container,
|
|
3
|
+
LoroDoc,
|
|
4
|
+
LoroMap,
|
|
5
|
+
Subscription,
|
|
6
|
+
Value,
|
|
7
|
+
} from "loro-crdt"
|
|
8
|
+
import type { LoroMapRef } from "../loro.js"
|
|
9
|
+
import type {
|
|
10
|
+
ContainerOrValueShape,
|
|
11
|
+
ContainerShape,
|
|
12
|
+
StructContainerShape,
|
|
13
|
+
ValueShape,
|
|
14
|
+
} from "../shape.js"
|
|
15
|
+
import { isValueShape } from "../utils/type-guards.js"
|
|
16
|
+
import { BaseRefInternals, type TypedRef, type TypedRefParams } from "./base.js"
|
|
17
|
+
import {
|
|
18
|
+
absorbCachedPlainValues,
|
|
19
|
+
assignPlainValueToTypedRef,
|
|
20
|
+
containerConstructor,
|
|
21
|
+
createContainerTypedRef,
|
|
22
|
+
hasContainerConstructor,
|
|
23
|
+
} from "./utils.js"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Internal implementation for StructRef.
|
|
27
|
+
* Contains all logic, state, and implementation details.
|
|
28
|
+
*/
|
|
29
|
+
export class StructRefInternals<
|
|
30
|
+
NestedShapes extends Record<string, ContainerOrValueShape>,
|
|
31
|
+
> extends BaseRefInternals<any> {
|
|
32
|
+
private propertyCache = new Map<string, TypedRef<ContainerShape> | Value>()
|
|
33
|
+
|
|
34
|
+
/** Get typed ref params for creating child refs at a key */
|
|
35
|
+
getTypedRefParams(
|
|
36
|
+
key: string,
|
|
37
|
+
shape: ContainerShape,
|
|
38
|
+
): TypedRefParams<ContainerShape> {
|
|
39
|
+
const placeholder = (this.getPlaceholder() as any)?.[key]
|
|
40
|
+
|
|
41
|
+
// AnyContainerShape is an escape hatch - it doesn't have a constructor
|
|
42
|
+
if (!hasContainerConstructor(shape._type)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Cannot create typed ref for shape type "${shape._type}". ` +
|
|
45
|
+
`Use Shape.any() only at the document root level.`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const LoroContainer = containerConstructor[shape._type]
|
|
50
|
+
const container = this.getContainer() as LoroMap
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
shape,
|
|
54
|
+
placeholder,
|
|
55
|
+
getContainer: () =>
|
|
56
|
+
container.getOrCreateContainer(key, new (LoroContainer as any)()),
|
|
57
|
+
autoCommit: this.getAutoCommit(),
|
|
58
|
+
batchedMutation: this.getBatchedMutation(),
|
|
59
|
+
getDoc: () => this.getDoc(),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get or create a ref for a key */
|
|
64
|
+
getOrCreateRef<Shape extends ContainerShape | ValueShape>(
|
|
65
|
+
key: string,
|
|
66
|
+
shape?: Shape,
|
|
67
|
+
): unknown {
|
|
68
|
+
const structShape = this.getShape() as StructContainerShape<NestedShapes>
|
|
69
|
+
const actualShape = shape || structShape.shapes[key]
|
|
70
|
+
const container = this.getContainer() as LoroMap
|
|
71
|
+
|
|
72
|
+
if (isValueShape(actualShape)) {
|
|
73
|
+
// When NOT in batchedMutation mode (direct access outside of change()), ALWAYS read fresh
|
|
74
|
+
// from container (NEVER cache). This ensures we always get the latest value
|
|
75
|
+
// from the CRDT, even when modified by a different ref instance (e.g., drafts from change())
|
|
76
|
+
//
|
|
77
|
+
// When in batchedMutation mode (inside change()), we cache value shapes so that
|
|
78
|
+
// mutations to nested objects persist back to the CRDT via absorbPlainValues()
|
|
79
|
+
if (!this.getBatchedMutation()) {
|
|
80
|
+
const containerValue = container.get(key)
|
|
81
|
+
if (containerValue !== undefined) {
|
|
82
|
+
return containerValue
|
|
83
|
+
}
|
|
84
|
+
// Only fall back to placeholder if the container doesn't have the value
|
|
85
|
+
const placeholder = (this.getPlaceholder() as any)?.[key]
|
|
86
|
+
if (placeholder === undefined) {
|
|
87
|
+
throw new Error("placeholder required")
|
|
88
|
+
}
|
|
89
|
+
return placeholder
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// In batched mode (within change()), we cache value shapes so that
|
|
93
|
+
// mutations to nested objects persist back to the CRDT via absorbPlainValues()
|
|
94
|
+
let ref = this.propertyCache.get(key)
|
|
95
|
+
if (!ref) {
|
|
96
|
+
const containerValue = container.get(key)
|
|
97
|
+
if (containerValue !== undefined) {
|
|
98
|
+
// For objects, create a deep copy so mutations can be tracked
|
|
99
|
+
if (typeof containerValue === "object" && containerValue !== null) {
|
|
100
|
+
ref = JSON.parse(JSON.stringify(containerValue))
|
|
101
|
+
} else {
|
|
102
|
+
ref = containerValue as Value
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Only fall back to placeholder if the container doesn't have the value
|
|
106
|
+
const placeholder = (this.getPlaceholder() as any)?.[key]
|
|
107
|
+
if (placeholder === undefined) {
|
|
108
|
+
throw new Error("placeholder required")
|
|
109
|
+
}
|
|
110
|
+
ref = placeholder as Value
|
|
111
|
+
}
|
|
112
|
+
this.propertyCache.set(key, ref)
|
|
113
|
+
}
|
|
114
|
+
return ref
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Container shapes: safe to cache (handles)
|
|
118
|
+
let ref = this.propertyCache.get(key)
|
|
119
|
+
if (!ref) {
|
|
120
|
+
ref = createContainerTypedRef(
|
|
121
|
+
this.getTypedRefParams(key, actualShape as ContainerShape),
|
|
122
|
+
)
|
|
123
|
+
this.propertyCache.set(key, ref)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return ref as Shape extends ContainerShape ? TypedRef<Shape> : Value
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Set a property value */
|
|
130
|
+
setPropertyValue(key: string, value: unknown): void {
|
|
131
|
+
const structShape = this.getShape() as StructContainerShape<NestedShapes>
|
|
132
|
+
const shape = structShape.shapes[key]
|
|
133
|
+
const container = this.getContainer() as LoroMap
|
|
134
|
+
|
|
135
|
+
if (!shape) {
|
|
136
|
+
throw new Error(`Unknown property: ${key}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isValueShape(shape)) {
|
|
140
|
+
container.set(key, value)
|
|
141
|
+
this.propertyCache.set(key, value as Value)
|
|
142
|
+
this.commitIfAuto()
|
|
143
|
+
} else {
|
|
144
|
+
// For container shapes, try to assign the plain value
|
|
145
|
+
const ref = this.getOrCreateRef(key, shape)
|
|
146
|
+
if (assignPlainValueToTypedRef(ref as TypedRef<any>, value)) {
|
|
147
|
+
this.commitIfAuto()
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
throw new Error(
|
|
151
|
+
"Cannot set container directly, modify the typed ref instead",
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Delete a property */
|
|
157
|
+
deleteProperty(key: string): void {
|
|
158
|
+
const container = this.getContainer() as LoroMap
|
|
159
|
+
container.delete(key)
|
|
160
|
+
this.propertyCache.delete(key)
|
|
161
|
+
this.commitIfAuto()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Absorb mutated plain values back into Loro containers */
|
|
165
|
+
absorbPlainValues(): void {
|
|
166
|
+
absorbCachedPlainValues(
|
|
167
|
+
this.propertyCache,
|
|
168
|
+
() => this.getContainer() as LoroMap,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Create the loro namespace for struct */
|
|
173
|
+
protected override createLoroNamespace(): LoroMapRef {
|
|
174
|
+
const self = this
|
|
175
|
+
return {
|
|
176
|
+
get doc(): LoroDoc {
|
|
177
|
+
return self.getDoc()
|
|
178
|
+
},
|
|
179
|
+
get container(): LoroMap {
|
|
180
|
+
return self.getContainer() as LoroMap
|
|
181
|
+
},
|
|
182
|
+
subscribe(callback: (event: unknown) => void): Subscription {
|
|
183
|
+
return (self.getContainer() as LoroMap).subscribe(callback)
|
|
184
|
+
},
|
|
185
|
+
setContainer(key: string, container: Container): Container {
|
|
186
|
+
const result = (self.getContainer() as LoroMap).setContainer(
|
|
187
|
+
key,
|
|
188
|
+
container,
|
|
189
|
+
)
|
|
190
|
+
self.commitIfAuto()
|
|
191
|
+
return result
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -123,9 +123,11 @@ describe("Struct value updates across change() calls", () => {
|
|
|
123
123
|
|
|
124
124
|
// Update the struct's value properties
|
|
125
125
|
change(doc, draft => {
|
|
126
|
-
const user = draft.users.
|
|
127
|
-
user
|
|
128
|
-
|
|
126
|
+
const user = draft.users.get("user1")
|
|
127
|
+
if (user) {
|
|
128
|
+
user.name = "Bob"
|
|
129
|
+
user.age = 25
|
|
130
|
+
}
|
|
129
131
|
})
|
|
130
132
|
expect(doc.users.user1?.name).toBe("Bob") // FAILS: returns "Alice"
|
|
131
133
|
expect(doc.users.user1?.age).toBe(25) // FAILS: returns 30
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { Container, LoroMap, Value } from "loro-crdt"
|
|
2
|
+
import { LORO_SYMBOL } from "../loro.js"
|
|
3
|
+
import type { ContainerOrValueShape, StructContainerShape } from "../shape.js"
|
|
4
|
+
import type { Infer } from "../types.js"
|
|
5
|
+
import {
|
|
6
|
+
INTERNAL_SYMBOL,
|
|
7
|
+
type RefInternalsBase,
|
|
8
|
+
TypedRef,
|
|
9
|
+
type TypedRefParams,
|
|
10
|
+
} from "./base.js"
|
|
11
|
+
import { StructRefInternals } from "./struct-ref-internals.js"
|
|
12
|
+
import { serializeRefToJSON } from "./utils.js"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal implementation class for struct containers.
|
|
16
|
+
* The actual StructRef is a Proxy wrapping this class.
|
|
17
|
+
*/
|
|
18
|
+
class StructRefImpl<
|
|
19
|
+
NestedShapes extends Record<string, ContainerOrValueShape>,
|
|
20
|
+
> extends TypedRef<any> {
|
|
21
|
+
[INTERNAL_SYMBOL]: StructRefInternals<NestedShapes>
|
|
22
|
+
|
|
23
|
+
constructor(params: TypedRefParams<any>) {
|
|
24
|
+
super()
|
|
25
|
+
this[INTERNAL_SYMBOL] = new StructRefInternals(params)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get structShape(): StructContainerShape<NestedShapes> {
|
|
29
|
+
return this[
|
|
30
|
+
INTERNAL_SYMBOL
|
|
31
|
+
].getShape() as StructContainerShape<NestedShapes>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toJSON(): Infer<StructContainerShape<NestedShapes>> {
|
|
35
|
+
return serializeRefToJSON(
|
|
36
|
+
this as any,
|
|
37
|
+
Object.keys(this.structShape.shapes),
|
|
38
|
+
) as Infer<StructContainerShape<NestedShapes>>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Deprecated methods - kept for backward compatibility
|
|
42
|
+
// @deprecated Use property access instead: obj.key
|
|
43
|
+
get(key: string): any {
|
|
44
|
+
const container = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
45
|
+
return container.get(key)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// @deprecated Use property assignment instead: obj.key = value
|
|
49
|
+
set(key: string, value: Value): void {
|
|
50
|
+
const container = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
51
|
+
container.set(key, value)
|
|
52
|
+
this[INTERNAL_SYMBOL].commitIfAuto()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// @deprecated Use loro(struct).setContainer() instead
|
|
56
|
+
setContainer<C extends Container>(key: string, container: C): C {
|
|
57
|
+
const loroContainer = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
58
|
+
const result = loroContainer.setContainer(key, container)
|
|
59
|
+
this[INTERNAL_SYMBOL].commitIfAuto()
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// @deprecated Use delete obj.key instead
|
|
64
|
+
delete(key: string): void {
|
|
65
|
+
const container = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
66
|
+
container.delete(key)
|
|
67
|
+
this[INTERNAL_SYMBOL].commitIfAuto()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// @deprecated Use 'key' in obj instead
|
|
71
|
+
has(key: string): boolean {
|
|
72
|
+
const container = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
73
|
+
return container.get(key) !== undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// @deprecated Use Object.keys(obj) instead
|
|
77
|
+
keys(): string[] {
|
|
78
|
+
const container = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
79
|
+
return container.keys()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// @deprecated Use Object.values(obj) instead
|
|
83
|
+
values(): any[] {
|
|
84
|
+
const container = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
85
|
+
return container.values()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// @deprecated Not standard for objects
|
|
89
|
+
get size(): number {
|
|
90
|
+
const container = this[INTERNAL_SYMBOL].getContainer() as LoroMap
|
|
91
|
+
return container.size
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a StructRef wrapped in a Proxy for JavaScript-native object behavior.
|
|
97
|
+
* Supports:
|
|
98
|
+
* - Property access: obj.key
|
|
99
|
+
* - Property assignment: obj.key = value
|
|
100
|
+
* - Object.keys(obj)
|
|
101
|
+
* - 'key' in obj
|
|
102
|
+
* - delete obj.key
|
|
103
|
+
* - toJSON()
|
|
104
|
+
* - loro(obj) for CRDT access
|
|
105
|
+
*/
|
|
106
|
+
export function createStructRef<
|
|
107
|
+
NestedShapes extends Record<string, ContainerOrValueShape>,
|
|
108
|
+
>(
|
|
109
|
+
params: TypedRefParams<StructContainerShape<NestedShapes>>,
|
|
110
|
+
): StructRef<NestedShapes> {
|
|
111
|
+
const impl = new StructRefImpl<NestedShapes>(params)
|
|
112
|
+
|
|
113
|
+
const proxy = new Proxy(impl, {
|
|
114
|
+
get(target, prop, receiver) {
|
|
115
|
+
// Handle Symbol access (loro(), internal, etc.)
|
|
116
|
+
if (prop === LORO_SYMBOL) {
|
|
117
|
+
return target[INTERNAL_SYMBOL].getLoroNamespace()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle INTERNAL_SYMBOL for internal methods
|
|
121
|
+
if (prop === INTERNAL_SYMBOL) {
|
|
122
|
+
return target[INTERNAL_SYMBOL]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Handle toJSON - use serializeRefToJSON with the proxy (receiver) so property access goes through the proxy
|
|
126
|
+
if (prop === "toJSON") {
|
|
127
|
+
return () =>
|
|
128
|
+
serializeRefToJSON(receiver, Object.keys(target.structShape.shapes))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Handle shape access (internal - needed for assignPlainValueToTypedRef)
|
|
132
|
+
if (prop === "shape") {
|
|
133
|
+
return target.structShape
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Schema property access
|
|
137
|
+
if (typeof prop === "string" && prop in target.structShape.shapes) {
|
|
138
|
+
const shape = target.structShape.shapes[prop]
|
|
139
|
+
return target[INTERNAL_SYMBOL].getOrCreateRef(prop, shape)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return undefined
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
set(target, prop, value) {
|
|
146
|
+
if (typeof prop === "string" && prop in target.structShape.shapes) {
|
|
147
|
+
target[INTERNAL_SYMBOL].setPropertyValue(prop, value)
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
has(target, prop) {
|
|
154
|
+
if (
|
|
155
|
+
prop === LORO_SYMBOL ||
|
|
156
|
+
prop === INTERNAL_SYMBOL ||
|
|
157
|
+
prop === "toJSON" ||
|
|
158
|
+
prop === "shape"
|
|
159
|
+
) {
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
if (typeof prop === "string") {
|
|
163
|
+
return prop in target.structShape.shapes
|
|
164
|
+
}
|
|
165
|
+
return false
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
deleteProperty(target, prop) {
|
|
169
|
+
if (typeof prop === "string" && prop in target.structShape.shapes) {
|
|
170
|
+
target[INTERNAL_SYMBOL].deleteProperty(prop)
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
return false
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
ownKeys(target) {
|
|
177
|
+
// Return only schema keys, not internal methods
|
|
178
|
+
return Object.keys(target.structShape.shapes)
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
182
|
+
if (typeof prop === "string" && prop in target.structShape.shapes) {
|
|
183
|
+
const shape = target.structShape.shapes[prop]
|
|
184
|
+
return {
|
|
185
|
+
configurable: true,
|
|
186
|
+
enumerable: true,
|
|
187
|
+
value: target[INTERNAL_SYMBOL].getOrCreateRef(prop, shape),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return undefined
|
|
191
|
+
},
|
|
192
|
+
}) as unknown as StructRef<NestedShapes>
|
|
193
|
+
|
|
194
|
+
return proxy
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Typed ref for struct containers (objects with fixed keys).
|
|
199
|
+
* Uses LoroMap as the underlying container.
|
|
200
|
+
*
|
|
201
|
+
* Supports JavaScript-native object behavior:
|
|
202
|
+
* - Property access: obj.key
|
|
203
|
+
* - Property assignment: obj.key = value
|
|
204
|
+
* - Object.keys(obj)
|
|
205
|
+
* - 'key' in obj
|
|
206
|
+
* - delete obj.key
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const schema = Shape.doc({
|
|
211
|
+
* settings: Shape.struct({
|
|
212
|
+
* darkMode: Shape.plain.boolean().placeholder(false),
|
|
213
|
+
* fontSize: Shape.plain.number().placeholder(14),
|
|
214
|
+
* }),
|
|
215
|
+
* });
|
|
216
|
+
*
|
|
217
|
+
* const doc = createTypedDoc(schema);
|
|
218
|
+
*
|
|
219
|
+
* // Property access
|
|
220
|
+
* doc.settings.darkMode = true;
|
|
221
|
+
* console.log(doc.settings.darkMode); // true
|
|
222
|
+
*
|
|
223
|
+
* // Object.keys()
|
|
224
|
+
* console.log(Object.keys(doc.settings)); // ['darkMode', 'fontSize']
|
|
225
|
+
*
|
|
226
|
+
* // 'key' in obj
|
|
227
|
+
* console.log('darkMode' in doc.settings); // true
|
|
228
|
+
*
|
|
229
|
+
* // delete obj.key
|
|
230
|
+
* delete doc.settings.darkMode;
|
|
231
|
+
*
|
|
232
|
+
* // CRDT access via loro()
|
|
233
|
+
* import { loro } from "@loro-extended/change";
|
|
234
|
+
* loro(doc.settings).setContainer('nested', loroMap);
|
|
235
|
+
* loro(doc.settings).subscribe(callback);
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export type StructRef<
|
|
239
|
+
NestedShapes extends Record<string, ContainerOrValueShape>,
|
|
240
|
+
> = {
|
|
241
|
+
[K in keyof NestedShapes]: NestedShapes[K]["_mutable"]
|
|
242
|
+
} & {
|
|
243
|
+
/**
|
|
244
|
+
* Serializes the struct to a plain JSON-compatible object.
|
|
245
|
+
*/
|
|
246
|
+
toJSON(): Infer<StructContainerShape<NestedShapes>>
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Internal methods accessed via INTERNAL_SYMBOL.
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
[INTERNAL_SYMBOL]: RefInternalsBase
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Re-export for backward compatibility
|
|
256
|
+
// The old class-based StructRef is now replaced by the proxy-based version
|
|
257
|
+
export { StructRefImpl as StructRefClass }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { LoroDoc, LoroText, Subscription } from "loro-crdt"
|
|
2
|
+
import type { LoroTextRef } from "../loro.js"
|
|
3
|
+
import type { TextContainerShape } from "../shape.js"
|
|
4
|
+
import { BaseRefInternals } from "./base.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal implementation for TextRef.
|
|
8
|
+
* Contains all logic, state, and implementation details.
|
|
9
|
+
*/
|
|
10
|
+
export class TextRefInternals extends BaseRefInternals<TextContainerShape> {
|
|
11
|
+
private materialized = false
|
|
12
|
+
|
|
13
|
+
/** Insert text at the given index */
|
|
14
|
+
insert(index: number, content: string): void {
|
|
15
|
+
this.materialized = true
|
|
16
|
+
;(this.getContainer() as LoroText).insert(index, content)
|
|
17
|
+
this.commitIfAuto()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Delete text at the given index */
|
|
21
|
+
delete(index: number, len: number): void {
|
|
22
|
+
this.materialized = true
|
|
23
|
+
;(this.getContainer() as LoroText).delete(index, len)
|
|
24
|
+
this.commitIfAuto()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Update the entire text content */
|
|
28
|
+
update(text: string): void {
|
|
29
|
+
this.materialized = true
|
|
30
|
+
;(this.getContainer() as LoroText).update(text)
|
|
31
|
+
this.commitIfAuto()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Mark a range of text with a key-value pair */
|
|
35
|
+
mark(range: { start: number; end: number }, key: string, value: any): void {
|
|
36
|
+
this.materialized = true
|
|
37
|
+
;(this.getContainer() as LoroText).mark(range, key, value)
|
|
38
|
+
this.commitIfAuto()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Remove a mark from a range of text */
|
|
42
|
+
unmark(range: { start: number; end: number }, key: string): void {
|
|
43
|
+
this.materialized = true
|
|
44
|
+
;(this.getContainer() as LoroText).unmark(range, key)
|
|
45
|
+
this.commitIfAuto()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Apply a delta to the text */
|
|
49
|
+
applyDelta(delta: any[]): void {
|
|
50
|
+
this.materialized = true
|
|
51
|
+
;(this.getContainer() as LoroText).applyDelta(delta)
|
|
52
|
+
this.commitIfAuto()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Get the text as a string */
|
|
56
|
+
getStringValue(): string {
|
|
57
|
+
const container = this.getContainer() as LoroText
|
|
58
|
+
const containerValue = container.toString()
|
|
59
|
+
if (containerValue !== "" || this.materialized) {
|
|
60
|
+
return containerValue
|
|
61
|
+
}
|
|
62
|
+
// Return placeholder if available and container is at default state
|
|
63
|
+
const placeholder = this.getPlaceholder()
|
|
64
|
+
if (placeholder !== undefined) {
|
|
65
|
+
return placeholder as string
|
|
66
|
+
}
|
|
67
|
+
return containerValue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Get the text as a delta */
|
|
71
|
+
toDelta(): any[] {
|
|
72
|
+
return (this.getContainer() as LoroText).toDelta()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Get the length of the text */
|
|
76
|
+
getLength(): number {
|
|
77
|
+
return (this.getContainer() as LoroText).length
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** No plain values in text */
|
|
81
|
+
absorbPlainValues(): void {
|
|
82
|
+
// no plain values contained within
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Create the loro namespace for text */
|
|
86
|
+
protected override createLoroNamespace(): LoroTextRef {
|
|
87
|
+
const self = this
|
|
88
|
+
return {
|
|
89
|
+
get doc(): LoroDoc {
|
|
90
|
+
return self.getDoc()
|
|
91
|
+
},
|
|
92
|
+
get container(): LoroText {
|
|
93
|
+
return self.getContainer() as LoroText
|
|
94
|
+
},
|
|
95
|
+
subscribe(callback: (event: unknown) => void): Subscription {
|
|
96
|
+
return (self.getContainer() as LoroText).subscribe(callback)
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { TextContainerShape } from "../shape.js"
|
|
2
|
+
import { INTERNAL_SYMBOL, TypedRef, type TypedRefParams } from "./base.js"
|
|
3
|
+
import { TextRefInternals } from "./text-ref-internals.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Text typed ref - thin facade that delegates to TextRefInternals.
|
|
7
|
+
*/
|
|
8
|
+
export class TextRef extends TypedRef<TextContainerShape> {
|
|
9
|
+
[INTERNAL_SYMBOL]: TextRefInternals
|
|
10
|
+
|
|
11
|
+
constructor(params: TypedRefParams<TextContainerShape>) {
|
|
12
|
+
super()
|
|
13
|
+
this[INTERNAL_SYMBOL] = new TextRefInternals(params)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Insert text at the given index */
|
|
17
|
+
insert(index: number, content: string): void {
|
|
18
|
+
this[INTERNAL_SYMBOL].insert(index, content)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Delete text at the given index */
|
|
22
|
+
delete(index: number, len: number): void {
|
|
23
|
+
this[INTERNAL_SYMBOL].delete(index, len)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Update the entire text content */
|
|
27
|
+
update(text: string): void {
|
|
28
|
+
this[INTERNAL_SYMBOL].update(text)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Mark a range of text with a key-value pair */
|
|
32
|
+
mark(range: { start: number; end: number }, key: string, value: any): void {
|
|
33
|
+
this[INTERNAL_SYMBOL].mark(range, key, value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Remove a mark from a range of text */
|
|
37
|
+
unmark(range: { start: number; end: number }, key: string): void {
|
|
38
|
+
this[INTERNAL_SYMBOL].unmark(range, key)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Apply a delta to the text */
|
|
42
|
+
applyDelta(delta: any[]): void {
|
|
43
|
+
this[INTERNAL_SYMBOL].applyDelta(delta)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Get the text as a string */
|
|
47
|
+
toString(): string {
|
|
48
|
+
return this[INTERNAL_SYMBOL].getStringValue()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
valueOf(): string {
|
|
52
|
+
return this.toString()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
toJSON(): string {
|
|
56
|
+
return this.toString()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
[Symbol.toPrimitive](_hint: string): string {
|
|
60
|
+
return this.toString()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get the text as a delta */
|
|
64
|
+
toDelta(): any[] {
|
|
65
|
+
return this[INTERNAL_SYMBOL].toDelta()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Get the length of the text */
|
|
69
|
+
get length(): number {
|
|
70
|
+
return this[INTERNAL_SYMBOL].getLength()
|
|
71
|
+
}
|
|
72
|
+
}
|