@loro-extended/change 0.9.0 → 1.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 (44) hide show
  1. package/README.md +179 -69
  2. package/dist/index.d.ts +369 -172
  3. package/dist/index.js +691 -382
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/change.test.ts +180 -175
  7. package/src/conversion.test.ts +91 -91
  8. package/src/conversion.ts +12 -12
  9. package/src/derive-placeholder.test.ts +14 -14
  10. package/src/derive-placeholder.ts +3 -3
  11. package/src/discriminated-union-assignability.test.ts +7 -7
  12. package/src/discriminated-union-tojson.test.ts +13 -24
  13. package/src/discriminated-union.test.ts +9 -8
  14. package/src/equality.test.ts +10 -2
  15. package/src/functional-helpers.test.ts +149 -0
  16. package/src/functional-helpers.ts +61 -0
  17. package/src/grand-unified-api.test.ts +423 -0
  18. package/src/index.ts +8 -6
  19. package/src/json-patch.test.ts +64 -56
  20. package/src/overlay-recursion.test.ts +326 -0
  21. package/src/overlay.ts +54 -17
  22. package/src/readonly.test.ts +27 -26
  23. package/src/shape.ts +103 -21
  24. package/src/typed-doc.ts +227 -58
  25. package/src/typed-refs/base.ts +33 -1
  26. package/src/typed-refs/counter.test.ts +44 -13
  27. package/src/typed-refs/counter.ts +42 -5
  28. package/src/typed-refs/doc.ts +29 -30
  29. package/src/typed-refs/json-compatibility.test.ts +37 -32
  30. package/src/typed-refs/list-base.ts +49 -21
  31. package/src/typed-refs/list.test.ts +4 -3
  32. package/src/typed-refs/movable-list.test.ts +3 -2
  33. package/src/typed-refs/movable-list.ts +6 -3
  34. package/src/typed-refs/proxy-handlers.ts +14 -1
  35. package/src/typed-refs/record.test.ts +116 -51
  36. package/src/typed-refs/record.ts +86 -81
  37. package/src/typed-refs/{map.ts → struct.ts} +66 -78
  38. package/src/typed-refs/text.ts +48 -7
  39. package/src/typed-refs/tree.ts +3 -3
  40. package/src/typed-refs/utils.ts +120 -13
  41. package/src/types.test.ts +34 -39
  42. package/src/types.ts +5 -40
  43. package/src/utils/type-guards.ts +11 -6
  44. package/src/validation.ts +10 -10
@@ -1,40 +1,29 @@
1
- import {
2
- type Container,
3
- LoroCounter,
4
- LoroList,
5
- LoroMap,
6
- LoroMovableList,
7
- LoroText,
8
- LoroTree,
9
- type Value,
10
- } from "loro-crdt"
1
+ import type { Container, LoroMap, Value } from "loro-crdt"
11
2
  import { deriveShapePlaceholder } from "../derive-placeholder.js"
3
+ import { mergeValue } from "../overlay.js"
12
4
  import type {
13
5
  ContainerOrValueShape,
14
6
  ContainerShape,
15
7
  RecordContainerShape,
16
8
  } from "../shape.js"
17
- import type { Infer, InferDraftType } from "../types.js"
9
+ import type { Infer, InferMutableType } from "../types.js"
18
10
  import { isContainerShape, isValueShape } from "../utils/type-guards.js"
19
11
  import { TypedRef, type TypedRefParams } from "./base.js"
20
- import { assignPlainValueToTypedRef, createContainerTypedRef } from "./utils.js"
21
-
22
- const containerConstructor = {
23
- counter: LoroCounter,
24
- list: LoroList,
25
- map: LoroMap,
26
- movableList: LoroMovableList,
27
- record: LoroMap,
28
- text: LoroText,
29
- tree: LoroTree,
30
- } as const
12
+ import {
13
+ absorbCachedPlainValues,
14
+ assignPlainValueToTypedRef,
15
+ containerConstructor,
16
+ createContainerTypedRef,
17
+ serializeRefToJSON,
18
+ unwrapReadonlyPrimitive,
19
+ } from "./utils.js"
31
20
 
32
21
  // Record typed ref
33
22
  export class RecordRef<
34
23
  NestedShape extends ContainerOrValueShape,
35
24
  > extends TypedRef<any> {
36
25
  [key: string]: Infer<NestedShape> | any
37
- private nodeCache = new Map<string, TypedRef<ContainerShape> | Value>()
26
+ private refCache = new Map<string, TypedRef<ContainerShape> | Value>()
38
27
 
39
28
  protected get shape(): RecordContainerShape<NestedShape> {
40
29
  return super.shape as RecordContainerShape<NestedShape>
@@ -45,16 +34,7 @@ export class RecordRef<
45
34
  }
46
35
 
47
36
  absorbPlainValues() {
48
- for (const [key, node] of this.nodeCache.entries()) {
49
- if (node instanceof TypedRef) {
50
- // Contains a TypedRef, not a plain Value: keep recursing
51
- node.absorbPlainValues()
52
- continue
53
- }
54
-
55
- // Plain value!
56
- this.container.set(key, node)
57
- }
37
+ absorbCachedPlainValues(this.refCache, () => this.container)
58
38
  }
59
39
 
60
40
  getTypedRefParams<S extends ContainerShape>(
@@ -79,85 +59,94 @@ export class RecordRef<
79
59
  getContainer: () =>
80
60
  this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
81
61
  readonly: this.readonly,
62
+ autoCommit: this._params.autoCommit,
63
+ getDoc: this._params.getDoc,
82
64
  }
83
65
  }
84
66
 
85
- getOrCreateNode(key: string): any {
86
- // For readonly mode with container shapes, check if the key exists first
67
+ /**
68
+ * Gets an existing ref for a key, or returns undefined if the key doesn't exist.
69
+ * Used for reading operations where we want optional chaining to work.
70
+ */
71
+ getRef(key: string): any {
72
+ // For container shapes, check if the key exists first
87
73
  // This allows optional chaining (?.) to work correctly for non-existent keys
88
- // Similar to how ListRefBase.getMutableItem() handles non-existent indices
89
- if (this.readonly && isContainerShape(this.shape.shape)) {
74
+ if (isContainerShape(this.shape.shape)) {
90
75
  const existing = this.container.get(key)
91
76
  if (existing === undefined) {
92
77
  return undefined
93
78
  }
94
79
  }
95
80
 
96
- let node = this.nodeCache.get(key)
97
- if (!node) {
81
+ return this.getOrCreateRef(key)
82
+ }
83
+
84
+ /**
85
+ * Gets or creates a ref for a key.
86
+ * Always creates the container if it doesn't exist.
87
+ * This is the method used for write operations.
88
+ */
89
+ getOrCreateRef(key: string): any {
90
+ let ref = this.refCache.get(key)
91
+ if (!ref) {
98
92
  const shape = this.shape.shape
99
93
  if (isContainerShape(shape)) {
100
- node = createContainerTypedRef(
94
+ ref = createContainerTypedRef(
101
95
  this.getTypedRefParams(key, shape as ContainerShape),
102
96
  )
103
- // Cache container nodes
104
- this.nodeCache.set(key, node)
97
+ // Cache container refs
98
+ this.refCache.set(key, ref)
105
99
  } else {
106
100
  // For value shapes, first try to get the value from the container
107
101
  const containerValue = this.container.get(key)
108
102
  if (containerValue !== undefined) {
109
- node = containerValue as Value
103
+ ref = containerValue as Value
110
104
  } else {
111
105
  // Only fall back to placeholder if the container doesn't have the value
112
106
  const placeholder = (this.placeholder as any)?.[key]
113
107
  if (placeholder === undefined) {
114
108
  // If it's a value type and not in container or placeholder,
115
109
  // fallback to the default value from the shape
116
- node = (shape as any)._plain
110
+ ref = (shape as any)._plain
117
111
  } else {
118
- node = placeholder as Value
112
+ ref = placeholder as Value
119
113
  }
120
114
  }
121
115
  // Only cache primitive values if NOT readonly
122
- if (node !== undefined && !this.readonly) {
123
- this.nodeCache.set(key, node)
116
+ if (ref !== undefined && !this.readonly) {
117
+ this.refCache.set(key, ref)
124
118
  }
125
119
  }
126
120
  }
127
121
 
128
122
  if (this.readonly && isContainerShape(this.shape.shape)) {
129
- const shape = this.shape.shape as ContainerShape
130
- if (shape._type === "counter") {
131
- return (node as any).value
132
- }
133
- if (shape._type === "text") {
134
- return (node as any).toString()
135
- }
123
+ return unwrapReadonlyPrimitive(
124
+ ref as TypedRef<any>,
125
+ this.shape.shape as ContainerShape,
126
+ )
136
127
  }
137
128
 
138
- return node as any
129
+ return ref as any
139
130
  }
140
131
 
141
- get(key: string): InferDraftType<NestedShape> {
142
- return this.getOrCreateNode(key)
132
+ get(key: string): InferMutableType<NestedShape> {
133
+ return this.getRef(key)
143
134
  }
144
135
 
145
136
  set(key: string, value: any): void {
146
- if (this.readonly) throw new Error("Cannot modify readonly ref")
137
+ this.assertMutable()
147
138
  if (isValueShape(this.shape.shape)) {
148
139
  this.container.set(key, value)
149
- this.nodeCache.set(key, value)
140
+ this.refCache.set(key, value)
141
+ this.commitIfAuto()
150
142
  } else {
151
- // For containers, we can't set them directly usually.
152
- // But if the user passes a plain object that matches the shape, maybe we should convert it?
153
- if (value && typeof value === "object") {
154
- const node = this.getOrCreateNode(key)
155
-
156
- if (assignPlainValueToTypedRef(node, value)) {
157
- return
158
- }
143
+ // For container shapes, try to assign the plain value
144
+ // Use getOrCreateRef to ensure the container is created
145
+ const ref = this.getOrCreateRef(key)
146
+ if (assignPlainValueToTypedRef(ref, value)) {
147
+ this.commitIfAuto()
148
+ return
159
149
  }
160
-
161
150
  throw new Error(
162
151
  "Cannot set container directly, modify the typed ref instead",
163
152
  )
@@ -165,14 +154,17 @@ export class RecordRef<
165
154
  }
166
155
 
167
156
  setContainer<C extends Container>(key: string, container: C): C {
168
- if (this.readonly) throw new Error("Cannot modify readonly ref")
169
- return this.container.setContainer(key, container)
157
+ this.assertMutable()
158
+ const result = this.container.setContainer(key, container)
159
+ this.commitIfAuto()
160
+ return result
170
161
  }
171
162
 
172
163
  delete(key: string): void {
173
- if (this.readonly) throw new Error("Cannot modify readonly ref")
164
+ this.assertMutable()
174
165
  this.container.delete(key)
175
- this.nodeCache.delete(key)
166
+ this.refCache.delete(key)
167
+ this.commitIfAuto()
176
168
  }
177
169
 
178
170
  has(key: string): boolean {
@@ -191,16 +183,29 @@ export class RecordRef<
191
183
  return this.container.size
192
184
  }
193
185
 
194
- toJSON(): Record<string, any> {
195
- const result: Record<string, any> = {}
196
- for (const key of this.keys()) {
197
- const value = this.get(key)
198
- if (value && typeof value === "object" && "toJSON" in value) {
199
- result[key] = (value as any).toJSON()
200
- } else {
201
- result[key] = value
186
+ toJSON(): Record<string, Infer<NestedShape>> {
187
+ // Fast path: readonly mode
188
+ if (this.readonly) {
189
+ const nativeJson = this.container.toJSON() as Record<string, any>
190
+ // For records, we need to overlay placeholders for each entry's nested shape
191
+ const result: Record<string, Infer<NestedShape>> = {}
192
+ for (const key of Object.keys(nativeJson)) {
193
+ // For records, the placeholder is always {}, so we need to derive
194
+ // the placeholder for the nested shape on the fly
195
+ const nestedPlaceholderValue = deriveShapePlaceholder(this.shape.shape)
196
+
197
+ result[key] = mergeValue(
198
+ this.shape.shape,
199
+ nativeJson[key],
200
+ nestedPlaceholderValue as Value,
201
+ ) as Infer<NestedShape>
202
202
  }
203
+ return result
203
204
  }
204
- return result
205
+
206
+ return serializeRefToJSON(this, this.keys()) as Record<
207
+ string,
208
+ Infer<NestedShape>
209
+ >
205
210
  }
206
211
  }
@@ -1,46 +1,39 @@
1
- import {
2
- type Container,
3
- LoroCounter,
4
- LoroList,
5
- LoroMap,
6
- LoroMovableList,
7
- LoroText,
8
- LoroTree,
9
- type Value,
10
- } from "loro-crdt"
1
+ import type { Container, LoroMap, Value } from "loro-crdt"
2
+ import { mergeValue } from "../overlay.js"
11
3
  import type {
12
4
  ContainerOrValueShape,
13
5
  ContainerShape,
14
- MapContainerShape,
6
+ StructContainerShape,
15
7
  ValueShape,
16
8
  } from "../shape.js"
9
+ import type { Infer } from "../types.js"
17
10
  import { isContainerShape, isValueShape } from "../utils/type-guards.js"
18
11
  import { TypedRef, type TypedRefParams } from "./base.js"
19
- import { assignPlainValueToTypedRef, createContainerTypedRef } from "./utils.js"
20
-
21
- const containerConstructor = {
22
- counter: LoroCounter,
23
- list: LoroList,
24
- map: LoroMap,
25
- movableList: LoroMovableList,
26
- record: LoroMap,
27
- text: LoroText,
28
- tree: LoroTree,
29
- } as const
30
-
31
- // Map typed ref
32
- export class MapRef<
12
+ import {
13
+ absorbCachedPlainValues,
14
+ assignPlainValueToTypedRef,
15
+ containerConstructor,
16
+ createContainerTypedRef,
17
+ serializeRefToJSON,
18
+ unwrapReadonlyPrimitive,
19
+ } from "./utils.js"
20
+
21
+ /**
22
+ * Typed ref for struct containers (objects with fixed keys).
23
+ * Uses LoroMap as the underlying container.
24
+ */
25
+ export class StructRef<
33
26
  NestedShapes extends Record<string, ContainerOrValueShape>,
34
27
  > extends TypedRef<any> {
35
28
  private propertyCache = new Map<string, TypedRef<ContainerShape> | Value>()
36
29
 
37
- constructor(params: TypedRefParams<MapContainerShape<NestedShapes>>) {
30
+ constructor(params: TypedRefParams<StructContainerShape<NestedShapes>>) {
38
31
  super(params)
39
32
  this.createLazyProperties()
40
33
  }
41
34
 
42
- protected get shape(): MapContainerShape<NestedShapes> {
43
- return super.shape as MapContainerShape<NestedShapes>
35
+ protected get shape(): StructContainerShape<NestedShapes> {
36
+ return super.shape as StructContainerShape<NestedShapes>
44
37
  }
45
38
 
46
39
  protected get container(): LoroMap {
@@ -48,16 +41,7 @@ export class MapRef<
48
41
  }
49
42
 
50
43
  absorbPlainValues() {
51
- for (const [key, node] of this.propertyCache.entries()) {
52
- if (node instanceof TypedRef) {
53
- // Contains a TypedRef, not a plain Value: keep recursing
54
- node.absorbPlainValues()
55
- continue
56
- }
57
-
58
- // Plain value!
59
- this.container.set(key, node)
60
- }
44
+ absorbCachedPlainValues(this.propertyCache, () => this.container)
61
45
  }
62
46
 
63
47
  getTypedRefParams<S extends ContainerShape>(
@@ -74,40 +58,42 @@ export class MapRef<
74
58
  getContainer: () =>
75
59
  this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
76
60
  readonly: this.readonly,
61
+ autoCommit: this._params.autoCommit,
62
+ getDoc: this._params.getDoc,
77
63
  }
78
64
  }
79
65
 
80
- getOrCreateNode<Shape extends ContainerShape | ValueShape>(
66
+ getOrCreateRef<Shape extends ContainerShape | ValueShape>(
81
67
  key: string,
82
68
  shape: Shape,
83
69
  ): any {
84
- let node = this.propertyCache.get(key)
85
- if (!node) {
70
+ let ref = this.propertyCache.get(key)
71
+ if (!ref) {
86
72
  if (isContainerShape(shape)) {
87
- node = createContainerTypedRef(this.getTypedRefParams(key, shape))
88
- // We cache container nodes even in readonly mode because they are just handles
89
- this.propertyCache.set(key, node)
73
+ ref = createContainerTypedRef(this.getTypedRefParams(key, shape))
74
+ // We cache container refs even in readonly mode because they are just handles
75
+ this.propertyCache.set(key, ref)
90
76
  } else {
91
77
  // For value shapes, first try to get the value from the container
92
78
  const containerValue = this.container.get(key)
93
79
  if (containerValue !== undefined) {
94
- node = containerValue as Value
80
+ ref = containerValue as Value
95
81
  } else {
96
82
  // Only fall back to placeholder if the container doesn't have the value
97
83
  const placeholder = (this.placeholder as any)?.[key]
98
84
  if (placeholder === undefined) {
99
85
  throw new Error("placeholder required")
100
86
  }
101
- node = placeholder as Value
87
+ ref = placeholder as Value
102
88
  }
103
89
 
104
90
  // In readonly mode, we DO NOT cache primitive values.
105
91
  // This ensures we always get the latest value from the CRDT on next access.
106
92
  if (!this.readonly) {
107
- this.propertyCache.set(key, node)
93
+ this.propertyCache.set(key, ref)
108
94
  }
109
95
  }
110
- if (node === undefined) throw new Error("no container made")
96
+ if (ref === undefined) throw new Error("no container made")
111
97
  }
112
98
 
113
99
  if (this.readonly && isContainerShape(shape)) {
@@ -118,34 +104,27 @@ export class MapRef<
118
104
  return (this.placeholder as any)?.[key]
119
105
  }
120
106
 
121
- if (shape._type === "counter") {
122
- return (node as any).value
123
- }
124
- if (shape._type === "text") {
125
- return (node as any).toString()
126
- }
107
+ return unwrapReadonlyPrimitive(ref as TypedRef<any>, shape)
127
108
  }
128
109
 
129
- return node as Shape extends ContainerShape ? TypedRef<Shape> : Value
110
+ return ref as Shape extends ContainerShape ? TypedRef<Shape> : Value
130
111
  }
131
112
 
132
113
  private createLazyProperties(): void {
133
114
  for (const key in this.shape.shapes) {
134
115
  const shape = this.shape.shapes[key]
135
116
  Object.defineProperty(this, key, {
136
- get: () => this.getOrCreateNode(key, shape),
117
+ get: () => this.getOrCreateRef(key, shape),
137
118
  set: value => {
138
- if (this.readonly) throw new Error("Cannot modify readonly ref")
119
+ this.assertMutable()
139
120
  if (isValueShape(shape)) {
140
121
  this.container.set(key, value)
141
122
  this.propertyCache.set(key, value)
142
123
  } else {
143
- if (value && typeof value === "object") {
144
- const node = this.getOrCreateNode(key, shape)
145
-
146
- if (assignPlainValueToTypedRef(node as TypedRef<any>, value)) {
147
- return
148
- }
124
+ // For container shapes, try to assign the plain value
125
+ const ref = this.getOrCreateRef(key, shape)
126
+ if (assignPlainValueToTypedRef(ref as TypedRef<any>, value)) {
127
+ return
149
128
  }
150
129
  throw new Error(
151
130
  "Cannot set container directly, modify the typed ref instead",
@@ -157,37 +136,46 @@ export class MapRef<
157
136
  }
158
137
  }
159
138
 
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
- }
139
+ toJSON(): Infer<StructContainerShape<NestedShapes>> {
140
+ // Fast path: readonly mode
141
+ if (this.readonly) {
142
+ const nativeJson = this.container.toJSON() as Value
143
+ // Overlay placeholders for missing properties
144
+ return mergeValue(
145
+ this.shape,
146
+ nativeJson,
147
+ this.placeholder as Value,
148
+ ) as Infer<StructContainerShape<NestedShapes>>
169
149
  }
170
- return result
150
+
151
+ return serializeRefToJSON(
152
+ this as any,
153
+ Object.keys(this.shape.shapes),
154
+ ) as Infer<StructContainerShape<NestedShapes>>
171
155
  }
172
156
 
173
- // TOOD(duane): return correct type here
157
+ // TODO(duane): return correct type here
174
158
  get(key: string): any {
175
159
  return this.container.get(key)
176
160
  }
177
161
 
178
162
  set(key: string, value: Value): void {
179
- if (this.readonly) throw new Error("Cannot modify readonly ref")
163
+ this.assertMutable()
180
164
  this.container.set(key, value)
165
+ this.commitIfAuto()
181
166
  }
182
167
 
183
168
  setContainer<C extends Container>(key: string, container: C): C {
184
- if (this.readonly) throw new Error("Cannot modify readonly ref")
185
- return this.container.setContainer(key, container)
169
+ this.assertMutable()
170
+ const result = this.container.setContainer(key, container)
171
+ this.commitIfAuto()
172
+ return result
186
173
  }
187
174
 
188
175
  delete(key: string): void {
189
- if (this.readonly) throw new Error("Cannot modify readonly ref")
176
+ this.assertMutable()
190
177
  this.container.delete(key)
178
+ this.commitIfAuto()
191
179
  }
192
180
 
193
181
  has(key: string): boolean {
@@ -1,44 +1,83 @@
1
+ import type { LoroText } from "loro-crdt"
1
2
  import type { TextContainerShape } from "../shape.js"
2
3
  import { TypedRef } from "./base.js"
3
4
 
4
5
  // Text typed ref
5
6
  export class TextRef extends TypedRef<TextContainerShape> {
7
+ // Track if we've materialized the container (made any changes)
8
+ private _materialized = false
9
+
10
+ protected get container(): LoroText {
11
+ return super.container as LoroText
12
+ }
13
+
6
14
  absorbPlainValues() {
7
15
  // no plain values contained within
8
16
  }
9
17
 
10
18
  // Text methods
11
19
  insert(index: number, content: string): void {
12
- if (this.readonly) throw new Error("Cannot modify readonly ref")
20
+ this.assertMutable()
21
+ this._materialized = true
13
22
  this.container.insert(index, content)
23
+ this.commitIfAuto()
14
24
  }
15
25
 
16
26
  delete(index: number, len: number): void {
17
- if (this.readonly) throw new Error("Cannot modify readonly ref")
27
+ this.assertMutable()
28
+ this._materialized = true
18
29
  this.container.delete(index, len)
30
+ this.commitIfAuto()
19
31
  }
20
32
 
33
+ /**
34
+ * Returns the text content.
35
+ * If the text hasn't been materialized (no operations performed),
36
+ * returns the placeholder value if available.
37
+ */
21
38
  toString(): string {
22
- return this.container.toString()
39
+ const containerValue = this.container.toString()
40
+ if (containerValue !== "" || this._materialized) {
41
+ return containerValue
42
+ }
43
+ // Return placeholder if available and container is at default state
44
+ if (this.placeholder !== undefined) {
45
+ return this.placeholder as string
46
+ }
47
+ return containerValue
48
+ }
49
+
50
+ valueOf(): string {
51
+ return this.toString()
23
52
  }
24
53
 
25
54
  toJSON(): string {
26
55
  return this.toString()
27
56
  }
28
57
 
58
+ [Symbol.toPrimitive](_hint: string): string {
59
+ return this.toString()
60
+ }
61
+
29
62
  update(text: string): void {
30
- if (this.readonly) throw new Error("Cannot modify readonly ref")
63
+ this.assertMutable()
64
+ this._materialized = true
31
65
  this.container.update(text)
66
+ this.commitIfAuto()
32
67
  }
33
68
 
34
69
  mark(range: { start: number; end: number }, key: string, value: any): void {
35
- if (this.readonly) throw new Error("Cannot modify readonly ref")
70
+ this.assertMutable()
71
+ this._materialized = true
36
72
  this.container.mark(range, key, value)
73
+ this.commitIfAuto()
37
74
  }
38
75
 
39
76
  unmark(range: { start: number; end: number }, key: string): void {
40
- if (this.readonly) throw new Error("Cannot modify readonly ref")
77
+ this.assertMutable()
78
+ this._materialized = true
41
79
  this.container.unmark(range, key)
80
+ this.commitIfAuto()
42
81
  }
43
82
 
44
83
  toDelta(): any[] {
@@ -46,8 +85,10 @@ export class TextRef extends TypedRef<TextContainerShape> {
46
85
  }
47
86
 
48
87
  applyDelta(delta: any[]): void {
49
- if (this.readonly) throw new Error("Cannot modify readonly ref")
88
+ this.assertMutable()
89
+ this._materialized = true
50
90
  this.container.applyDelta(delta)
91
+ this.commitIfAuto()
51
92
  }
52
93
 
53
94
  get length(): number {
@@ -8,17 +8,17 @@ export class TreeRef<T extends TreeContainerShape> extends TypedRef<T> {
8
8
  }
9
9
 
10
10
  createNode(parent?: any, index?: number): any {
11
- if (this.readonly) throw new Error("Cannot modify readonly ref")
11
+ this.assertMutable()
12
12
  return this.container.createNode(parent, index)
13
13
  }
14
14
 
15
15
  move(target: any, parent?: any, index?: number): void {
16
- if (this.readonly) throw new Error("Cannot modify readonly ref")
16
+ this.assertMutable()
17
17
  this.container.move(target, parent, index)
18
18
  }
19
19
 
20
20
  delete(target: any): void {
21
- if (this.readonly) throw new Error("Cannot modify readonly ref")
21
+ this.assertMutable()
22
22
  this.container.delete(target)
23
23
  }
24
24