@loro-extended/change 0.7.0 → 0.8.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.
@@ -0,0 +1,245 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { derivePlaceholder } from "./derive-placeholder.js"
3
+ import { Shape } from "./shape.js"
4
+
5
+ describe("derivePlaceholder", () => {
6
+ it("uses default values when no placeholder set", () => {
7
+ const schema = Shape.doc({
8
+ title: Shape.text(),
9
+ count: Shape.counter(),
10
+ })
11
+
12
+ expect(derivePlaceholder(schema)).toEqual({
13
+ title: "",
14
+ count: 0,
15
+ })
16
+ })
17
+
18
+ it("uses placeholder values when set", () => {
19
+ const schema = Shape.doc({
20
+ title: Shape.text().placeholder("Untitled"),
21
+ count: Shape.counter().placeholder(100),
22
+ })
23
+
24
+ expect(derivePlaceholder(schema)).toEqual({
25
+ title: "Untitled",
26
+ count: 100,
27
+ })
28
+ })
29
+
30
+ it("composes nested map placeholders", () => {
31
+ const schema = Shape.doc({
32
+ settings: Shape.map({
33
+ theme: Shape.plain.string().placeholder("dark"),
34
+ fontSize: Shape.plain.number().placeholder(14),
35
+ }),
36
+ })
37
+
38
+ expect(derivePlaceholder(schema)).toEqual({
39
+ settings: {
40
+ theme: "dark",
41
+ fontSize: 14,
42
+ },
43
+ })
44
+ })
45
+
46
+ it("uses empty arrays for lists", () => {
47
+ const schema = Shape.doc({
48
+ items: Shape.list(Shape.plain.string()),
49
+ })
50
+
51
+ expect(derivePlaceholder(schema)).toEqual({
52
+ items: [],
53
+ })
54
+ })
55
+
56
+ it("uses empty objects for records", () => {
57
+ const schema = Shape.doc({
58
+ data: Shape.record(Shape.plain.number()),
59
+ })
60
+
61
+ expect(derivePlaceholder(schema)).toEqual({
62
+ data: {},
63
+ })
64
+ })
65
+
66
+ it("handles plain value shapes with defaults", () => {
67
+ const schema = Shape.doc({
68
+ config: Shape.map({
69
+ name: Shape.plain.string(),
70
+ count: Shape.plain.number(),
71
+ enabled: Shape.plain.boolean(),
72
+ nothing: Shape.plain.null(),
73
+ }),
74
+ })
75
+
76
+ expect(derivePlaceholder(schema)).toEqual({
77
+ config: {
78
+ name: "",
79
+ count: 0,
80
+ enabled: false,
81
+ nothing: null,
82
+ },
83
+ })
84
+ })
85
+
86
+ it("handles plain value shapes with custom placeholders", () => {
87
+ const schema = Shape.doc({
88
+ config: Shape.map({
89
+ name: Shape.plain.string().placeholder("default-name"),
90
+ count: Shape.plain.number().placeholder(42),
91
+ enabled: Shape.plain.boolean().placeholder(true),
92
+ }),
93
+ })
94
+
95
+ expect(derivePlaceholder(schema)).toEqual({
96
+ config: {
97
+ name: "default-name",
98
+ count: 42,
99
+ enabled: true,
100
+ },
101
+ })
102
+ })
103
+
104
+ it("handles nested plain objects", () => {
105
+ const schema = Shape.doc({
106
+ user: Shape.map({
107
+ profile: Shape.plain.object({
108
+ name: Shape.plain.string().placeholder("Anonymous"),
109
+ age: Shape.plain.number().placeholder(0),
110
+ }),
111
+ }),
112
+ })
113
+
114
+ expect(derivePlaceholder(schema)).toEqual({
115
+ user: {
116
+ profile: {
117
+ name: "Anonymous",
118
+ age: 0,
119
+ },
120
+ },
121
+ })
122
+ })
123
+
124
+ it("handles plain arrays as empty", () => {
125
+ const schema = Shape.doc({
126
+ tags: Shape.map({
127
+ items: Shape.plain.array(Shape.plain.string()),
128
+ }),
129
+ })
130
+
131
+ expect(derivePlaceholder(schema)).toEqual({
132
+ tags: {
133
+ items: [],
134
+ },
135
+ })
136
+ })
137
+
138
+ it("handles plain records as empty", () => {
139
+ const schema = Shape.doc({
140
+ metadata: Shape.map({
141
+ values: Shape.plain.record(Shape.plain.number()),
142
+ }),
143
+ })
144
+
145
+ expect(derivePlaceholder(schema)).toEqual({
146
+ metadata: {
147
+ values: {},
148
+ },
149
+ })
150
+ })
151
+
152
+ it("handles union types by deriving from first variant", () => {
153
+ const schema = Shape.doc({
154
+ value: Shape.map({
155
+ data: Shape.plain.union([Shape.plain.string(), Shape.plain.null()]),
156
+ }),
157
+ })
158
+
159
+ expect(derivePlaceholder(schema)).toEqual({
160
+ value: {
161
+ data: "", // First variant is string, default is ""
162
+ },
163
+ })
164
+ })
165
+
166
+ it("handles union types with explicit placeholder", () => {
167
+ const schema = Shape.doc({
168
+ value: Shape.map({
169
+ data: Shape.plain
170
+ .union([Shape.plain.string(), Shape.plain.null()])
171
+ .placeholder(null),
172
+ }),
173
+ })
174
+
175
+ expect(derivePlaceholder(schema)).toEqual({
176
+ value: {
177
+ data: null,
178
+ },
179
+ })
180
+ })
181
+
182
+ it("handles movable lists as empty arrays", () => {
183
+ const schema = Shape.doc({
184
+ tasks: Shape.movableList(Shape.plain.string()),
185
+ })
186
+
187
+ expect(derivePlaceholder(schema)).toEqual({
188
+ tasks: [],
189
+ })
190
+ })
191
+
192
+ it("handles tree containers as empty arrays", () => {
193
+ const schema = Shape.doc({
194
+ hierarchy: Shape.tree(Shape.map({ name: Shape.text() })),
195
+ })
196
+
197
+ expect(derivePlaceholder(schema)).toEqual({
198
+ hierarchy: [],
199
+ })
200
+ })
201
+
202
+ it("handles complex nested structures", () => {
203
+ const schema = Shape.doc({
204
+ article: Shape.map({
205
+ title: Shape.text().placeholder("Untitled Article"),
206
+ metadata: Shape.map({
207
+ views: Shape.counter().placeholder(0),
208
+ author: Shape.plain.object({
209
+ name: Shape.plain.string().placeholder("Anonymous"),
210
+ email: Shape.plain.string(),
211
+ }),
212
+ tags: Shape.list(Shape.plain.string()),
213
+ }),
214
+ }),
215
+ })
216
+
217
+ expect(derivePlaceholder(schema)).toEqual({
218
+ article: {
219
+ title: "Untitled Article",
220
+ metadata: {
221
+ views: 0,
222
+ author: {
223
+ name: "Anonymous",
224
+ email: "",
225
+ },
226
+ tags: [],
227
+ },
228
+ },
229
+ })
230
+ })
231
+
232
+ it("handles string literal options", () => {
233
+ const schema = Shape.doc({
234
+ status: Shape.map({
235
+ value: Shape.plain.string("active", "inactive", "pending"),
236
+ }),
237
+ })
238
+
239
+ expect(derivePlaceholder(schema)).toEqual({
240
+ status: {
241
+ value: "active", // First option is the default
242
+ },
243
+ })
244
+ })
245
+ })
@@ -0,0 +1,132 @@
1
+ import type { ContainerOrValueShape, DocShape, ValueShape } from "./shape.js"
2
+ import type { InferPlaceholderType } from "./types.js"
3
+
4
+ /**
5
+ * Derives the placeholder state from a schema by composing placeholder values.
6
+ *
7
+ * For leaf nodes (text, counter, values): uses _placeholder directly
8
+ * For containers (map, list, record): recurses into nested shapes
9
+ */
10
+ export function derivePlaceholder<T extends DocShape>(
11
+ schema: T,
12
+ ): InferPlaceholderType<T> {
13
+ const result: Record<string, unknown> = {}
14
+
15
+ for (const [key, shape] of Object.entries(schema.shapes)) {
16
+ result[key] = deriveShapePlaceholder(shape)
17
+ }
18
+
19
+ return result as InferPlaceholderType<T>
20
+ }
21
+
22
+ /**
23
+ * Derives placeholder for a single shape.
24
+ *
25
+ * Leaf nodes: return _placeholder directly
26
+ * Containers: recurse into nested shapes (ignore _placeholder on containers)
27
+ */
28
+ export function deriveShapePlaceholder(shape: ContainerOrValueShape): unknown {
29
+ switch (shape._type) {
30
+ // Leaf containers - use _placeholder directly
31
+ case "text":
32
+ return shape._placeholder
33
+ case "counter":
34
+ return shape._placeholder
35
+
36
+ // Dynamic containers - always empty (no per-entry merging)
37
+ case "list":
38
+ case "movableList":
39
+ case "tree":
40
+ return []
41
+ case "record":
42
+ return {}
43
+
44
+ // Structured container - recurse into nested shapes
45
+ case "map": {
46
+ const result: Record<string, unknown> = {}
47
+ for (const [key, nestedShape] of Object.entries(shape.shapes)) {
48
+ result[key] = deriveShapePlaceholder(nestedShape)
49
+ }
50
+ return result
51
+ }
52
+
53
+ case "value":
54
+ return deriveValueShapePlaceholder(shape)
55
+
56
+ default:
57
+ return undefined
58
+ }
59
+ }
60
+
61
+ function deriveValueShapePlaceholder(shape: ValueShape): unknown {
62
+ switch (shape.valueType) {
63
+ // Leaf values - use _placeholder directly
64
+ case "string":
65
+ return shape._placeholder
66
+ case "number":
67
+ return shape._placeholder
68
+ case "boolean":
69
+ return shape._placeholder
70
+ case "null":
71
+ return null
72
+ case "undefined":
73
+ return undefined
74
+ case "uint8array":
75
+ return shape._placeholder
76
+
77
+ // Structured value - recurse into nested shapes (like map)
78
+ case "object": {
79
+ const result: Record<string, unknown> = {}
80
+ for (const [key, nestedShape] of Object.entries(shape.shape)) {
81
+ result[key] = deriveValueShapePlaceholder(nestedShape)
82
+ }
83
+ return result
84
+ }
85
+
86
+ // Dynamic values - always empty
87
+ case "array":
88
+ return []
89
+ case "record":
90
+ return {}
91
+
92
+ // Unions - use _placeholder if explicitly set, otherwise derive from first variant
93
+ case "union": {
94
+ // Check if _placeholder was explicitly set (not the default empty object)
95
+ // We need to check if it's a primitive value OR a non-empty object
96
+ const placeholder = shape._placeholder
97
+ if (placeholder !== undefined) {
98
+ // If it's a primitive (null, string, number, boolean), use it
99
+ if (placeholder === null || typeof placeholder !== "object") {
100
+ return placeholder
101
+ }
102
+ // If it's an object with keys, use it
103
+ if (Object.keys(placeholder as object).length > 0) {
104
+ return placeholder
105
+ }
106
+ }
107
+ // Otherwise derive from first variant
108
+ return deriveValueShapePlaceholder(shape.shapes[0])
109
+ }
110
+
111
+ case "discriminatedUnion": {
112
+ // Check if _placeholder was explicitly set (not the default empty object)
113
+ const placeholder = shape._placeholder
114
+ if (placeholder !== undefined) {
115
+ // If it's a primitive (null, string, number, boolean), use it
116
+ if (placeholder === null || typeof placeholder !== "object") {
117
+ return placeholder
118
+ }
119
+ // If it's an object with keys, use it
120
+ if (Object.keys(placeholder as object).length > 0) {
121
+ return placeholder
122
+ }
123
+ }
124
+ // Otherwise derive from first variant
125
+ const firstKey = Object.keys(shape.variants)[0]
126
+ return deriveValueShapePlaceholder(shape.variants[firstKey])
127
+ }
128
+
129
+ default:
130
+ return undefined
131
+ }
132
+ }
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { TypedDoc } from "./change.js"
3
2
  import { mergeValue } from "./overlay.js"
4
3
  import { Shape } from "./shape.js"
4
+ import { TypedDoc } from "./typed-doc.js"
5
5
  import { validateValue } from "./validation.js"
6
6
 
7
7
  describe("discriminatedUnion", () => {
@@ -107,7 +107,7 @@ describe("discriminatedUnion", () => {
107
107
  EmptyClientPresence,
108
108
  )
109
109
 
110
- // Should use client variant based on emptyState's type
110
+ // Should use client variant based on placeholder's type
111
111
  expect(result).toEqual({
112
112
  type: "client",
113
113
  name: "Bob",
@@ -216,15 +216,11 @@ describe("discriminatedUnion", () => {
216
216
  it("should allow setting a discriminated union property in a MapDraftNode", () => {
217
217
  const DocSchema = Shape.doc({
218
218
  state: Shape.map({
219
- presence: GamePresenceSchema,
219
+ presence: GamePresenceSchema.placeholder(EmptyClientPresence),
220
220
  }),
221
221
  })
222
222
 
223
- const doc = new TypedDoc(DocSchema, {
224
- state: {
225
- presence: EmptyClientPresence,
226
- },
227
- })
223
+ const doc = new TypedDoc(DocSchema)
228
224
 
229
225
  doc.change(draft => {
230
226
  // This should work now that MapDraftNode recognizes discriminatedUnion as a value shape
@@ -235,7 +231,7 @@ describe("discriminatedUnion", () => {
235
231
  }
236
232
  })
237
233
 
238
- expect(doc.value.state.presence).toEqual({
234
+ expect(doc.toJSON().state.presence).toEqual({
239
235
  type: "server",
240
236
  cars: { p1: { x: 10, y: 20 } },
241
237
  tick: 100,
@@ -1,10 +1,11 @@
1
1
  import type { ContainerShape, DocShape, ShapeToContainer } from "../shape.js"
2
- import type { InferPlainType } from "../types.js"
2
+ import type { Infer } from "../types.js"
3
3
 
4
4
  export type DraftNodeParams<Shape extends DocShape | ContainerShape> = {
5
5
  shape: Shape
6
- emptyState?: InferPlainType<Shape>
6
+ placeholder?: Infer<Shape>
7
7
  getContainer: () => ShapeToContainer<Shape>
8
+ readonly?: boolean
8
9
  }
9
10
 
10
11
  // Base class for all draft nodes
@@ -19,8 +20,12 @@ export abstract class DraftNode<Shape extends DocShape | ContainerShape> {
19
20
  return this._params.shape
20
21
  }
21
22
 
22
- protected get emptyState(): InferPlainType<Shape> | undefined {
23
- return this._params.emptyState
23
+ protected get placeholder(): Infer<Shape> | undefined {
24
+ return this._params.placeholder
25
+ }
26
+
27
+ protected get readonly(): boolean {
28
+ return !!this._params.readonly
24
29
  }
25
30
 
26
31
  protected get container(): ShapeToContainer<Shape> {
@@ -0,0 +1,31 @@
1
+ ### Summary: The `getCounter` Side-Effect & Read-Only Access
2
+
3
+ We have uncovered a subtle and unexpected between Loro's API and our `TypedDoc` "empty state" logic.
4
+
5
+ #### 1. The Problem
6
+
7
+ A test expects a LoroConter at `doc.value.counter` to return `1` (from `emptyState`), but it returns `0`.
8
+
9
+ - **`doc.toJSON()`** works correctly: It sees the doc is empty, so it overlays `emptyState` (`{ counter: 1 }`).
10
+ - **`doc.value`** fails: It returns `0`.
11
+
12
+ #### 2. The Root Cause: "Heisenberg" Observation
13
+
14
+ Accessing a root container in Loro changes it.
15
+
16
+ - **Observation:** Calling `doc.getCounter("counter")` **materializes** the container in the CRDT with a default value of `0`.
17
+ - **Consequence:** `DraftDoc` calls `getCounter` to read the value. This inadvertently "writes" a `0` to the document.
18
+ - **Result:** The "empty" state is lost. The document now effectively contains `counter: 0`.
19
+
20
+ #### 3. The Solution: `getShallowValue()`
21
+
22
+ We need to peek at the document to see if a container exists _before_ we try to retrieve (and thus create) it.
23
+
24
+ - **Investigation:** We verified that `doc.getShallowValue()` returns a list of existing root containers _without_ creating new ones.
25
+ - **Strategy:**
26
+ 1. In `DraftDoc` (which handles root properties), check `readonly` mode.
27
+ 2. Call `doc.getShallowValue()`.
28
+ 3. **If key is missing:** Return the `emptyState` value directly.
29
+ 4. **If key exists:** Safe to call `doc.getCounter()` (or others) to get the actual CRDT value.
30
+
31
+ This ensures `doc.value` remains a pure, non-destructive view that respects the "virtual" empty state.
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { Shape } from "../shape.js"
3
+ import { createTypedDoc } from "../typed-doc.js"
4
+
5
+ describe("Counter Draft Node", () => {
6
+ it("should return placeholder value without materializing the container", () => {
7
+ const schema = Shape.doc({
8
+ counter: Shape.counter().placeholder(10),
9
+ })
10
+ const doc = createTypedDoc(schema)
11
+
12
+ // Accessing the value should return placeholder
13
+ expect(doc.value.counter).toBe(10)
14
+
15
+ // Verify it is NOT materialized in the underlying doc
16
+ const shallow = doc.loroDoc.getShallowValue()
17
+ expect(shallow.counter).toBeUndefined()
18
+ })
19
+
20
+ it("should materialize the container after modification", () => {
21
+ const schema = Shape.doc({
22
+ counter: Shape.counter().placeholder(10),
23
+ })
24
+ const doc = createTypedDoc(schema)
25
+
26
+ doc.change(draft => {
27
+ draft.counter.increment(5)
28
+ })
29
+
30
+ // Value should be updated
31
+ // Note: placeholder is NOT applied to the CRDT. It is only a read-time overlay.
32
+ // When we modify the counter, we are modifying the underlying CRDT counter which starts at 0.
33
+ // So 0 + 5 = 5. The placeholder (10) is lost once the container exists.
34
+ expect(doc.value.counter).toBe(5)
35
+
36
+ // Verify it IS materialized in the underlying doc
37
+ const shallow = doc.loroDoc.getShallowValue()
38
+ expect(shallow.counter).toBeDefined()
39
+ })
40
+
41
+ it("should respect placeholder even if accessed multiple times", () => {
42
+ const schema = Shape.doc({
43
+ counter: Shape.counter().placeholder(10),
44
+ })
45
+ const doc = createTypedDoc(schema)
46
+
47
+ expect(doc.value.counter).toBe(10)
48
+ expect(doc.value.counter).toBe(10)
49
+
50
+ // Still not materialized
51
+ expect(doc.loroDoc.getShallowValue().counter).toBeUndefined()
52
+ })
53
+ })
@@ -1,5 +1,5 @@
1
1
  import type { LoroDoc } from "loro-crdt"
2
- import type { InferPlainType } from "../index.js"
2
+ import type { Infer } from "../index.js"
3
3
  import type { ContainerShape, DocShape } from "../shape.js"
4
4
  import { DraftNode, type DraftNodeParams } from "./base.js"
5
5
  import { createContainerDraftNode } from "./utils.js"
@@ -18,7 +18,7 @@ const containerGetter = {
18
18
  export class DraftDoc<Shape extends DocShape> extends DraftNode<Shape> {
19
19
  private doc: LoroDoc
20
20
  private propertyCache = new Map<string, DraftNode<ContainerShape>>()
21
- private requiredEmptyState!: InferPlainType<Shape>
21
+ private requiredPlaceholder!: Infer<Shape>
22
22
 
23
23
  constructor(
24
24
  _params: Omit<DraftNodeParams<Shape>, "getContainer"> & { doc: LoroDoc },
@@ -29,9 +29,9 @@ export class DraftDoc<Shape extends DocShape> extends DraftNode<Shape> {
29
29
  throw new Error("can't get container on DraftDoc")
30
30
  },
31
31
  })
32
- if (!_params.emptyState) throw new Error("emptyState required")
32
+ if (!_params.placeholder) throw new Error("placeholder required")
33
33
  this.doc = _params.doc
34
- this.requiredEmptyState = _params.emptyState
34
+ this.requiredPlaceholder = _params.placeholder
35
35
  this.createLazyProperties()
36
36
  }
37
37
 
@@ -43,15 +43,27 @@ export class DraftDoc<Shape extends DocShape> extends DraftNode<Shape> {
43
43
 
44
44
  return {
45
45
  shape,
46
- emptyState: this.requiredEmptyState[key],
46
+ placeholder: this.requiredPlaceholder[key],
47
47
  getContainer: () => getter(key),
48
+ readonly: this.readonly,
48
49
  }
49
50
  }
50
51
 
51
52
  getOrCreateDraftNode(
52
53
  key: string,
53
54
  shape: ContainerShape,
54
- ): DraftNode<ContainerShape> {
55
+ ): DraftNode<ContainerShape> | number | string {
56
+ if (
57
+ this.readonly &&
58
+ (shape._type === "counter" || shape._type === "text")
59
+ ) {
60
+ // Check if the container exists in the doc without creating it
61
+ const shallow = this.doc.getShallowValue()
62
+ if (!shallow[key]) {
63
+ return this.requiredPlaceholder[key] as any
64
+ }
65
+ }
66
+
55
67
  let node = this.propertyCache.get(key)
56
68
 
57
69
  if (!node) {
@@ -59,6 +71,15 @@ export class DraftDoc<Shape extends DocShape> extends DraftNode<Shape> {
59
71
  this.propertyCache.set(key, node)
60
72
  }
61
73
 
74
+ if (this.readonly) {
75
+ if (shape._type === "counter") {
76
+ return (node as any).value
77
+ }
78
+ if (shape._type === "text") {
79
+ return (node as any).toString()
80
+ }
81
+ }
82
+
62
83
  return node
63
84
  }
64
85
 
@@ -84,7 +84,7 @@ export abstract class ListDraftNodeBase<
84
84
  ): DraftNodeParams<ContainerShape> {
85
85
  return {
86
86
  shape,
87
- emptyState: undefined, // List items don't have empty state
87
+ placeholder: undefined, // List items don't have placeholder
88
88
  getContainer: () => {
89
89
  const containerItem = this.container.get(index)
90
90
  if (!containerItem || !isContainer(containerItem)) {
@@ -92,6 +92,7 @@ export abstract class ListDraftNodeBase<
92
92
  }
93
93
  return containerItem
94
94
  },
95
+ readonly: this.readonly,
95
96
  }
96
97
  }
97
98
 
@@ -142,7 +143,7 @@ export abstract class ListDraftNodeBase<
142
143
  }
143
144
 
144
145
  // Get item for return values - returns DraftItem that can be mutated
145
- protected getDraftItem(index: number): DraftItem {
146
+ protected getDraftItem(index: number): any {
146
147
  // Check if we already have a cached item for this index
147
148
  let cachedItem = this.itemCache.get(index)
148
149
  if (cachedItem) {
@@ -167,14 +168,29 @@ export abstract class ListDraftNodeBase<
167
168
  // For primitives, just use the value directly
168
169
  cachedItem = containerItem
169
170
  }
170
- this.itemCache.set(index, cachedItem)
171
+ // Only cache primitive values if NOT readonly
172
+ if (!this.readonly) {
173
+ this.itemCache.set(index, cachedItem)
174
+ }
171
175
  return cachedItem as DraftItem
172
176
  } else {
173
177
  // For container shapes, create a proper draft node using the new pattern
174
178
  cachedItem = createContainerDraftNode(
175
179
  this.getDraftNodeParams(index, this.shape.shape as ContainerShape),
176
180
  )
181
+ // Cache container nodes
177
182
  this.itemCache.set(index, cachedItem)
183
+
184
+ if (this.readonly) {
185
+ const shape = this.shape.shape as ContainerShape
186
+ if (shape._type === "counter") {
187
+ return (cachedItem as any).value
188
+ }
189
+ if (shape._type === "text") {
190
+ return (cachedItem as any).toString()
191
+ }
192
+ }
193
+
178
194
  return cachedItem as DraftItem
179
195
  }
180
196
  }
@@ -254,26 +270,31 @@ export abstract class ListDraftNodeBase<
254
270
  }
255
271
 
256
272
  insert(index: number, item: Item): void {
273
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
257
274
  // Update cache indices before performing the insert operation
258
275
  this.updateCacheForInsert(index)
259
276
  this.insertWithConversion(index, item)
260
277
  }
261
278
 
262
279
  delete(index: number, len: number): void {
280
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
263
281
  // Update cache indices before performing the delete operation
264
282
  this.updateCacheForDelete(index, len)
265
283
  this.container.delete(index, len)
266
284
  }
267
285
 
268
286
  push(item: Item): void {
287
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
269
288
  this.pushWithConversion(item)
270
289
  }
271
290
 
272
291
  pushContainer(container: Container): Container {
292
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
273
293
  return this.container.pushContainer(container)
274
294
  }
275
295
 
276
296
  insertContainer(index: number, container: Container): Container {
297
+ if (this.readonly) throw new Error("Cannot modify readonly doc")
277
298
  return this.container.insertContainer(index, container)
278
299
  }
279
300