@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.
Files changed (56) hide show
  1. package/README.md +173 -149
  2. package/dist/index.d.ts +962 -335
  3. package/dist/index.js +1040 -598
  4. package/dist/index.js.map +1 -1
  5. package/package.json +4 -4
  6. package/src/change.test.ts +51 -52
  7. package/src/functional-helpers.test.ts +316 -4
  8. package/src/functional-helpers.ts +96 -6
  9. package/src/grand-unified-api.test.ts +35 -29
  10. package/src/index.ts +25 -1
  11. package/src/json-patch.test.ts +46 -27
  12. package/src/loro.test.ts +449 -0
  13. package/src/loro.ts +273 -0
  14. package/src/overlay-recursion.test.ts +1 -1
  15. package/src/path-evaluator.ts +1 -1
  16. package/src/path-selector.test.ts +94 -1
  17. package/src/shape.ts +47 -15
  18. package/src/typed-doc.ts +99 -98
  19. package/src/typed-refs/base.ts +126 -35
  20. package/src/typed-refs/counter-ref-internals.ts +62 -0
  21. package/src/typed-refs/{counter.test.ts → counter-ref.test.ts} +5 -4
  22. package/src/typed-refs/counter-ref.ts +45 -0
  23. package/src/typed-refs/{doc.ts → doc-ref-internals.ts} +33 -38
  24. package/src/typed-refs/doc-ref.ts +47 -0
  25. package/src/typed-refs/encapsulation.test.ts +226 -0
  26. package/src/typed-refs/list-ref-base-internals.ts +280 -0
  27. package/src/typed-refs/{list-base.ts → list-ref-base.ts} +255 -160
  28. package/src/typed-refs/list-ref-internals.ts +21 -0
  29. package/src/typed-refs/{list.ts → list-ref.ts} +10 -11
  30. package/src/typed-refs/movable-list-ref-internals.ts +38 -0
  31. package/src/typed-refs/movable-list-ref.ts +31 -0
  32. package/src/typed-refs/proxy-handlers.ts +13 -4
  33. package/src/typed-refs/{record.ts → record-ref-internals.ts} +78 -79
  34. package/src/typed-refs/{record.test.ts → record-ref.test.ts} +21 -16
  35. package/src/typed-refs/record-ref.ts +80 -0
  36. package/src/typed-refs/struct-ref-internals.ts +195 -0
  37. package/src/typed-refs/{struct-value-updates.test.ts → struct-ref.test.ts} +5 -3
  38. package/src/typed-refs/struct-ref.ts +257 -0
  39. package/src/typed-refs/text-ref-internals.ts +100 -0
  40. package/src/typed-refs/text-ref.ts +72 -0
  41. package/src/typed-refs/tree-node-ref-internals.ts +111 -0
  42. package/src/typed-refs/{tree-node.ts → tree-node-ref.ts} +58 -94
  43. package/src/typed-refs/tree-ref-internals.ts +110 -0
  44. package/src/typed-refs/tree-ref.ts +194 -0
  45. package/src/typed-refs/utils.ts +21 -23
  46. package/src/typed-refs/counter.ts +0 -62
  47. package/src/typed-refs/movable-list.ts +0 -32
  48. package/src/typed-refs/struct.ts +0 -201
  49. package/src/typed-refs/text.ts +0 -91
  50. package/src/typed-refs/tree.ts +0 -268
  51. /package/src/typed-refs/{list-value-updates.test.ts → list-ref-value-updates.test.ts} +0 -0
  52. /package/src/typed-refs/{list.test.ts → list-ref.test.ts} +0 -0
  53. /package/src/typed-refs/{movable-list.test.ts → movable-list-ref.test.ts} +0 -0
  54. /package/src/typed-refs/{record-value-updates.test.ts → record-ref-value-updates.test.ts} +0 -0
  55. /package/src/typed-refs/{tree-node-value-updates.test.ts → tree-node-ref.test.ts} +0 -0
  56. /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.getOrCreateRef("user1")
127
- user.name = "Bob"
128
- user.age = 25
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
+ }