@loro-extended/change 0.9.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +201 -93
  2. package/dist/index.d.ts +361 -169
  3. package/dist/index.js +516 -235
  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 +19 -19
  8. package/src/conversion.ts +7 -7
  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 +23 -22
  21. package/src/overlay.ts +9 -9
  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 +23 -1
  26. package/src/typed-refs/counter.test.ts +44 -13
  27. package/src/typed-refs/counter.ts +40 -3
  28. package/src/typed-refs/doc.ts +12 -6
  29. package/src/typed-refs/json-compatibility.test.ts +37 -32
  30. package/src/typed-refs/list-base.ts +26 -22
  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 +4 -1
  34. package/src/typed-refs/proxy-handlers.ts +14 -1
  35. package/src/typed-refs/record.test.ts +107 -42
  36. package/src/typed-refs/record.ts +37 -19
  37. package/src/typed-refs/{map.ts → struct.ts} +31 -16
  38. package/src/typed-refs/text.ts +42 -1
  39. package/src/typed-refs/utils.ts +28 -6
  40. package/src/types.test.ts +34 -39
  41. package/src/types.ts +5 -40
  42. package/src/utils/type-guards.ts +11 -6
  43. package/src/validation.ts +10 -10
@@ -5,7 +5,8 @@ import type { RecordRef } from "./record.js"
5
5
  export const recordProxyHandler: ProxyHandler<RecordRef<any>> = {
6
6
  get: (target, prop) => {
7
7
  if (typeof prop === "string" && !(prop in target)) {
8
- return target.get(prop)
8
+ // Use getRef for reading - returns undefined for non-existent keys
9
+ return target.getRef(prop)
9
10
  }
10
11
  return Reflect.get(target, prop)
11
12
  },
@@ -23,6 +24,18 @@ export const recordProxyHandler: ProxyHandler<RecordRef<any>> = {
23
24
  }
24
25
  return Reflect.deleteProperty(target, prop)
25
26
  },
27
+ // Support `in` operator for checking key existence
28
+ has: (target, prop) => {
29
+ if (typeof prop === "string") {
30
+ // Check if it's a method/property on the class first
31
+ if (prop in target) {
32
+ return true
33
+ }
34
+ // Otherwise check the underlying container
35
+ return target.has(prop)
36
+ }
37
+ return Reflect.has(target, prop)
38
+ },
26
39
  ownKeys: target => {
27
40
  return target.keys()
28
41
  },
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { Shape, TypedDoc } from "../index.js"
2
+ import { change } from "../functional-helpers.js"
3
+ import { createTypedDoc, Shape } from "../index.js"
3
4
 
4
5
  describe("Record Types", () => {
5
6
  describe("Shape.record (Container)", () => {
@@ -8,9 +9,9 @@ describe("Record Types", () => {
8
9
  scores: Shape.record(Shape.counter()),
9
10
  })
10
11
 
11
- const doc = new TypedDoc(schema)
12
+ const doc = createTypedDoc(schema)
12
13
 
13
- doc.change(draft => {
14
+ change(doc, draft => {
14
15
  draft.scores.getOrCreateRef("alice").increment(10)
15
16
  draft.scores.getOrCreateRef("bob").increment(5)
16
17
  })
@@ -20,7 +21,7 @@ describe("Record Types", () => {
20
21
  bob: 5,
21
22
  })
22
23
 
23
- doc.change(draft => {
24
+ change(doc, draft => {
24
25
  draft.scores.getOrCreateRef("alice").increment(5)
25
26
  draft.scores.delete("bob")
26
27
  })
@@ -35,9 +36,9 @@ describe("Record Types", () => {
35
36
  notes: Shape.record(Shape.text()),
36
37
  })
37
38
 
38
- const doc = new TypedDoc(schema)
39
+ const doc = createTypedDoc(schema)
39
40
 
40
- doc.change(draft => {
41
+ change(doc, draft => {
41
42
  draft.notes.getOrCreateRef("todo").insert(0, "Buy milk")
42
43
  draft.notes.getOrCreateRef("reminders").insert(0, "Call mom")
43
44
  })
@@ -53,9 +54,9 @@ describe("Record Types", () => {
53
54
  groups: Shape.record(Shape.list(Shape.plain.string())),
54
55
  })
55
56
 
56
- const doc = new TypedDoc(schema)
57
+ const doc = createTypedDoc(schema)
57
58
 
58
- doc.change(draft => {
59
+ change(doc, draft => {
59
60
  const groupA = draft.groups.getOrCreateRef("groupA")
60
61
  groupA.push("alice")
61
62
  groupA.push("bob")
@@ -74,14 +75,14 @@ describe("Record Types", () => {
74
75
  describe("Shape.plain.record (Value)", () => {
75
76
  it("should handle record of plain strings", () => {
76
77
  const schema = Shape.doc({
77
- wrapper: Shape.map({
78
+ wrapper: Shape.struct({
78
79
  config: Shape.plain.record(Shape.plain.string()),
79
80
  }),
80
81
  })
81
82
 
82
- const doc = new TypedDoc(schema)
83
+ const doc = createTypedDoc(schema)
83
84
 
84
- doc.change(draft => {
85
+ change(doc, draft => {
85
86
  draft.wrapper.config.theme = "dark"
86
87
  draft.wrapper.config.lang = "en"
87
88
  })
@@ -91,7 +92,7 @@ describe("Record Types", () => {
91
92
  lang: "en",
92
93
  })
93
94
 
94
- doc.change(draft => {
95
+ change(doc, draft => {
95
96
  delete draft.wrapper.config.theme
96
97
  draft.wrapper.config.lang = "fr"
97
98
  })
@@ -103,14 +104,14 @@ describe("Record Types", () => {
103
104
 
104
105
  it("should handle record of plain numbers", () => {
105
106
  const schema = Shape.doc({
106
- wrapper: Shape.map({
107
+ wrapper: Shape.struct({
107
108
  stats: Shape.plain.record(Shape.plain.number()),
108
109
  }),
109
110
  })
110
111
 
111
- const doc = new TypedDoc(schema)
112
+ const doc = createTypedDoc(schema)
112
113
 
113
- doc.change(draft => {
114
+ change(doc, draft => {
114
115
  draft.wrapper.stats.visits = 100
115
116
  draft.wrapper.stats.clicks = 50
116
117
  })
@@ -123,16 +124,16 @@ describe("Record Types", () => {
123
124
 
124
125
  it("should handle nested records", () => {
125
126
  const schema = Shape.doc({
126
- wrapper: Shape.map({
127
+ wrapper: Shape.struct({
127
128
  settings: Shape.plain.record(
128
129
  Shape.plain.record(Shape.plain.boolean()),
129
130
  ),
130
131
  }),
131
132
  })
132
133
 
133
- const doc = new TypedDoc(schema)
134
+ const doc = createTypedDoc(schema)
134
135
 
135
- doc.change(draft => {
136
+ change(doc, draft => {
136
137
  draft.wrapper.settings.ui = {
137
138
  darkMode: true,
138
139
  sidebar: false,
@@ -160,16 +161,16 @@ describe("Record Types", () => {
160
161
  it("should handle record of maps", () => {
161
162
  const schema = Shape.doc({
162
163
  users: Shape.record(
163
- Shape.map({
164
+ Shape.struct({
164
165
  name: Shape.plain.string(),
165
166
  age: Shape.plain.number(),
166
167
  }),
167
168
  ),
168
169
  })
169
170
 
170
- const doc = new TypedDoc(schema)
171
+ const doc = createTypedDoc(schema)
171
172
 
172
- doc.change(draft => {
173
+ change(doc, draft => {
173
174
  const alice = draft.users.getOrCreateRef("u1")
174
175
  alice.name = "Alice"
175
176
  alice.age = 30
@@ -188,7 +189,7 @@ describe("Record Types", () => {
188
189
  it("should allow setting a plain object for a record with map values", () => {
189
190
  const schema = Shape.doc({
190
191
  participants: Shape.record(
191
- Shape.map({
192
+ Shape.struct({
192
193
  id: Shape.plain.string(),
193
194
  role: Shape.plain.string(),
194
195
  name: Shape.plain.string(),
@@ -197,9 +198,9 @@ describe("Record Types", () => {
197
198
  ),
198
199
  })
199
200
 
200
- const doc = new TypedDoc(schema)
201
+ const doc = createTypedDoc(schema)
201
202
 
202
- doc.change(draft => {
203
+ change(doc, draft => {
203
204
  draft.participants["student-1"] = {
204
205
  id: "student-1",
205
206
  role: "student",
@@ -219,17 +220,17 @@ describe("Record Types", () => {
219
220
  it("should allow setting a plain object for a record with nested map values", () => {
220
221
  const schema = Shape.doc({
221
222
  data: Shape.record(
222
- Shape.map({
223
- info: Shape.map({
223
+ Shape.struct({
224
+ info: Shape.struct({
224
225
  name: Shape.plain.string(),
225
226
  }),
226
227
  }),
227
228
  ),
228
229
  })
229
230
 
230
- const doc = new TypedDoc(schema)
231
+ const doc = createTypedDoc(schema)
231
232
 
232
- doc.change(draft => {
233
+ change(doc, draft => {
233
234
  draft.data["item-1"] = {
234
235
  info: {
235
236
  name: "Item 1",
@@ -249,15 +250,15 @@ describe("Record Types", () => {
249
250
  histories: Shape.record(Shape.list(Shape.plain.string())),
250
251
  })
251
252
 
252
- const doc = new TypedDoc(schema)
253
+ const doc = createTypedDoc(schema)
253
254
 
254
- doc.change(draft => {
255
+ change(doc, draft => {
255
256
  draft.histories.user1 = ["a", "b"]
256
257
  })
257
258
 
258
259
  expect(doc.toJSON().histories.user1).toEqual(["a", "b"])
259
260
 
260
- doc.change(draft => {
261
+ change(doc, draft => {
261
262
  // biome-ignore lint/complexity/useLiteralKeys: tests indexed assignment
262
263
  draft.histories["user1"] = ["c"]
263
264
  })
@@ -265,6 +266,70 @@ describe("Record Types", () => {
265
266
  // biome-ignore lint/complexity/useLiteralKeys: tests indexed assignment
266
267
  expect(doc.toJSON().histories["user1"]).toEqual(["c"])
267
268
  })
269
+
270
+ it("should allow setting a plain string for a record of text", () => {
271
+ const schema = Shape.doc({
272
+ notes: Shape.record(Shape.text()),
273
+ })
274
+
275
+ const doc = createTypedDoc(schema)
276
+
277
+ change(doc, draft => {
278
+ draft.notes.set("note-1", "Hello World")
279
+ draft.notes["note-2"] = "Another note"
280
+ })
281
+
282
+ expect(doc.toJSON().notes).toEqual({
283
+ "note-1": "Hello World",
284
+ "note-2": "Another note",
285
+ })
286
+ })
287
+
288
+ it("should allow setting a plain number for a record of counter", () => {
289
+ const schema = Shape.doc({
290
+ scores: Shape.record(Shape.counter()),
291
+ })
292
+
293
+ const doc = createTypedDoc(schema)
294
+
295
+ change(doc, draft => {
296
+ draft.scores.set("alice", 100)
297
+ draft.scores.bob = 50
298
+ })
299
+
300
+ expect(doc.toJSON().scores).toEqual({
301
+ alice: 100,
302
+ bob: 50,
303
+ })
304
+ })
305
+
306
+ it("should allow setting a plain object with text fields for a record of maps", () => {
307
+ const schema = Shape.doc({
308
+ users: Shape.record(
309
+ Shape.struct({
310
+ userId: Shape.plain.string(),
311
+ displayName: Shape.text(),
312
+ email: Shape.plain.string(),
313
+ }),
314
+ ),
315
+ })
316
+
317
+ const doc = createTypedDoc(schema)
318
+
319
+ change(doc, draft => {
320
+ draft.users.set("user-123", {
321
+ userId: "user-123",
322
+ displayName: "Test User",
323
+ email: "test@example.com",
324
+ })
325
+ })
326
+
327
+ expect(doc.toJSON().users["user-123"]).toEqual({
328
+ userId: "user-123",
329
+ displayName: "Test User",
330
+ email: "test@example.com",
331
+ })
332
+ })
268
333
  })
269
334
 
270
335
  describe("Readonly access to non-existent keys", () => {
@@ -273,26 +338,26 @@ describe("Record Types", () => {
273
338
  // preferences: Record<string, { showTip: boolean }>
274
339
  const schema = Shape.doc({
275
340
  preferences: Shape.record(
276
- Shape.map({
341
+ Shape.struct({
277
342
  showTip: Shape.plain.boolean(),
278
343
  }),
279
344
  ),
280
345
  })
281
346
 
282
- const doc = new TypedDoc(schema)
347
+ const doc = createTypedDoc(schema)
283
348
 
284
349
  // First, set a value for a specific peer
285
- doc.change(d => {
350
+ change(doc, d => {
286
351
  d.preferences.peer1 = { showTip: true }
287
352
  })
288
353
 
289
354
  // This should work - accessing an existing key
290
- expect(doc.value.preferences.peer1?.showTip).toBe(true)
355
+ expect(doc.preferences.peer1?.showTip).toBe(true)
291
356
 
292
357
  // Accessing a non-existent key should NOT throw "placeholder required"
293
358
  // It should return undefined so optional chaining works correctly
294
359
  expect(() => {
295
- const result = doc.value.preferences.nonexistent?.showTip
360
+ const result = doc.preferences.nonexistent?.showTip
296
361
  return result
297
362
  }).not.toThrow()
298
363
  })
@@ -300,16 +365,16 @@ describe("Record Types", () => {
300
365
  it("should return undefined for non-existent record keys in readonly mode", () => {
301
366
  const schema = Shape.doc({
302
367
  preferences: Shape.record(
303
- Shape.map({
368
+ Shape.struct({
304
369
  showTip: Shape.plain.boolean(),
305
370
  }),
306
371
  ),
307
372
  })
308
373
 
309
- const doc = new TypedDoc(schema)
374
+ const doc = createTypedDoc(schema)
310
375
 
311
376
  // Access a key that doesn't exist - should return undefined
312
- const prefs = doc.value.preferences.nonexistent
377
+ const prefs = doc.preferences.nonexistent
313
378
  expect(prefs).toBeUndefined()
314
379
  })
315
380
 
@@ -317,19 +382,19 @@ describe("Record Types", () => {
317
382
  // Exact reproduction of a user's schema and access pattern
318
383
  const schema = Shape.doc({
319
384
  preferences: Shape.record(
320
- Shape.map({
385
+ Shape.struct({
321
386
  showTip: Shape.plain.boolean(),
322
387
  }),
323
388
  ),
324
389
  })
325
390
 
326
- const doc = new TypedDoc(schema)
391
+ const doc = createTypedDoc(schema)
327
392
  const myPeerId = "some-peer-id"
328
393
 
329
394
  // This is the exact code pattern from the user's app:
330
395
  // doc.preferences[myPeerId]?.showTip !== false
331
396
  expect(() => {
332
- const showTip = doc.value.preferences[myPeerId]?.showTip
397
+ const showTip = doc.preferences[myPeerId]?.showTip
333
398
  const result = showTip !== false
334
399
  return result
335
400
  }).not.toThrow()
@@ -59,20 +59,34 @@ export class RecordRef<
59
59
  getContainer: () =>
60
60
  this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
61
61
  readonly: this.readonly,
62
+ autoCommit: this._params.autoCommit,
63
+ getDoc: this._params.getDoc,
62
64
  }
63
65
  }
64
66
 
65
- getOrCreateRef(key: string): any {
66
- // 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
67
73
  // This allows optional chaining (?.) to work correctly for non-existent keys
68
- // Similar to how ListRefBase.getMutableItem() handles non-existent indices
69
- if (this.readonly && isContainerShape(this.shape.shape)) {
74
+ if (isContainerShape(this.shape.shape)) {
70
75
  const existing = this.container.get(key)
71
76
  if (existing === undefined) {
72
77
  return undefined
73
78
  }
74
79
  }
75
80
 
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 {
76
90
  let ref = this.refCache.get(key)
77
91
  if (!ref) {
78
92
  const shape = this.shape.shape
@@ -116,7 +130,7 @@ export class RecordRef<
116
130
  }
117
131
 
118
132
  get(key: string): InferMutableType<NestedShape> {
119
- return this.getOrCreateRef(key)
133
+ return this.getRef(key)
120
134
  }
121
135
 
122
136
  set(key: string, value: any): void {
@@ -124,17 +138,15 @@ export class RecordRef<
124
138
  if (isValueShape(this.shape.shape)) {
125
139
  this.container.set(key, value)
126
140
  this.refCache.set(key, value)
141
+ this.commitIfAuto()
127
142
  } else {
128
- // For containers, we can't set them directly usually.
129
- // But if the user passes a plain object that matches the shape, maybe we should convert it?
130
- if (value && typeof value === "object") {
131
- const ref = this.getOrCreateRef(key)
132
-
133
- if (assignPlainValueToTypedRef(ref, value)) {
134
- return
135
- }
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
136
149
  }
137
-
138
150
  throw new Error(
139
151
  "Cannot set container directly, modify the typed ref instead",
140
152
  )
@@ -143,13 +155,16 @@ export class RecordRef<
143
155
 
144
156
  setContainer<C extends Container>(key: string, container: C): C {
145
157
  this.assertMutable()
146
- return this.container.setContainer(key, container)
158
+ const result = this.container.setContainer(key, container)
159
+ this.commitIfAuto()
160
+ return result
147
161
  }
148
162
 
149
163
  delete(key: string): void {
150
164
  this.assertMutable()
151
165
  this.container.delete(key)
152
166
  this.refCache.delete(key)
167
+ this.commitIfAuto()
153
168
  }
154
169
 
155
170
  has(key: string): boolean {
@@ -168,12 +183,12 @@ export class RecordRef<
168
183
  return this.container.size
169
184
  }
170
185
 
171
- toJSON(): Record<string, any> {
186
+ toJSON(): Record<string, Infer<NestedShape>> {
172
187
  // Fast path: readonly mode
173
188
  if (this.readonly) {
174
189
  const nativeJson = this.container.toJSON() as Record<string, any>
175
190
  // For records, we need to overlay placeholders for each entry's nested shape
176
- const result: Record<string, any> = {}
191
+ const result: Record<string, Infer<NestedShape>> = {}
177
192
  for (const key of Object.keys(nativeJson)) {
178
193
  // For records, the placeholder is always {}, so we need to derive
179
194
  // the placeholder for the nested shape on the fly
@@ -183,11 +198,14 @@ export class RecordRef<
183
198
  this.shape.shape,
184
199
  nativeJson[key],
185
200
  nestedPlaceholderValue as Value,
186
- )
201
+ ) as Infer<NestedShape>
187
202
  }
188
203
  return result
189
204
  }
190
205
 
191
- return serializeRefToJSON(this, this.keys())
206
+ return serializeRefToJSON(this, this.keys()) as Record<
207
+ string,
208
+ Infer<NestedShape>
209
+ >
192
210
  }
193
211
  }
@@ -3,9 +3,10 @@ import { mergeValue } from "../overlay.js"
3
3
  import type {
4
4
  ContainerOrValueShape,
5
5
  ContainerShape,
6
- MapContainerShape,
6
+ StructContainerShape,
7
7
  ValueShape,
8
8
  } from "../shape.js"
9
+ import type { Infer } from "../types.js"
9
10
  import { isContainerShape, isValueShape } from "../utils/type-guards.js"
10
11
  import { TypedRef, type TypedRefParams } from "./base.js"
11
12
  import {
@@ -17,19 +18,22 @@ import {
17
18
  unwrapReadonlyPrimitive,
18
19
  } from "./utils.js"
19
20
 
20
- // Map typed ref
21
- export class MapRef<
21
+ /**
22
+ * Typed ref for struct containers (objects with fixed keys).
23
+ * Uses LoroMap as the underlying container.
24
+ */
25
+ export class StructRef<
22
26
  NestedShapes extends Record<string, ContainerOrValueShape>,
23
27
  > extends TypedRef<any> {
24
28
  private propertyCache = new Map<string, TypedRef<ContainerShape> | Value>()
25
29
 
26
- constructor(params: TypedRefParams<MapContainerShape<NestedShapes>>) {
30
+ constructor(params: TypedRefParams<StructContainerShape<NestedShapes>>) {
27
31
  super(params)
28
32
  this.createLazyProperties()
29
33
  }
30
34
 
31
- protected get shape(): MapContainerShape<NestedShapes> {
32
- return super.shape as MapContainerShape<NestedShapes>
35
+ protected get shape(): StructContainerShape<NestedShapes> {
36
+ return super.shape as StructContainerShape<NestedShapes>
33
37
  }
34
38
 
35
39
  protected get container(): LoroMap {
@@ -54,6 +58,8 @@ export class MapRef<
54
58
  getContainer: () =>
55
59
  this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
56
60
  readonly: this.readonly,
61
+ autoCommit: this._params.autoCommit,
62
+ getDoc: this._params.getDoc,
57
63
  }
58
64
  }
59
65
 
@@ -115,12 +121,10 @@ export class MapRef<
115
121
  this.container.set(key, value)
116
122
  this.propertyCache.set(key, value)
117
123
  } else {
118
- if (value && typeof value === "object") {
119
- const ref = this.getOrCreateRef(key, shape)
120
-
121
- if (assignPlainValueToTypedRef(ref as TypedRef<any>, value)) {
122
- return
123
- }
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
124
128
  }
125
129
  throw new Error(
126
130
  "Cannot set container directly, modify the typed ref instead",
@@ -132,15 +136,22 @@ export class MapRef<
132
136
  }
133
137
  }
134
138
 
135
- toJSON(): any {
139
+ toJSON(): Infer<StructContainerShape<NestedShapes>> {
136
140
  // Fast path: readonly mode
137
141
  if (this.readonly) {
138
142
  const nativeJson = this.container.toJSON() as Value
139
143
  // Overlay placeholders for missing properties
140
- return mergeValue(this.shape, nativeJson, this.placeholder as Value)
144
+ return mergeValue(
145
+ this.shape,
146
+ nativeJson,
147
+ this.placeholder as Value,
148
+ ) as Infer<StructContainerShape<NestedShapes>>
141
149
  }
142
150
 
143
- return serializeRefToJSON(this as any, Object.keys(this.shape.shapes))
151
+ return serializeRefToJSON(
152
+ this as any,
153
+ Object.keys(this.shape.shapes),
154
+ ) as Infer<StructContainerShape<NestedShapes>>
144
155
  }
145
156
 
146
157
  // TODO(duane): return correct type here
@@ -151,16 +162,20 @@ export class MapRef<
151
162
  set(key: string, value: Value): void {
152
163
  this.assertMutable()
153
164
  this.container.set(key, value)
165
+ this.commitIfAuto()
154
166
  }
155
167
 
156
168
  setContainer<C extends Container>(key: string, container: C): C {
157
169
  this.assertMutable()
158
- return this.container.setContainer(key, container)
170
+ const result = this.container.setContainer(key, container)
171
+ this.commitIfAuto()
172
+ return result
159
173
  }
160
174
 
161
175
  delete(key: string): void {
162
176
  this.assertMutable()
163
177
  this.container.delete(key)
178
+ this.commitIfAuto()
164
179
  }
165
180
 
166
181
  has(key: string): boolean {