@loro-extended/change 0.9.1 → 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 (43) hide show
  1. package/README.md +179 -69
  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
@@ -1,8 +1,16 @@
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
  }
@@ -10,35 +18,66 @@ export class TextRef extends TypedRef<TextContainerShape> {
10
18
  // Text methods
11
19
  insert(index: number, content: string): void {
12
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
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
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
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
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[] {
@@ -47,7 +86,9 @@ export class TextRef extends TypedRef<TextContainerShape> {
47
86
 
48
87
  applyDelta(delta: any[]): void {
49
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 {
@@ -11,16 +11,15 @@ import type {
11
11
  ContainerShape,
12
12
  CounterContainerShape,
13
13
  ListContainerShape,
14
- MapContainerShape,
15
14
  MovableListContainerShape,
16
15
  RecordContainerShape,
16
+ StructContainerShape,
17
17
  TextContainerShape,
18
18
  TreeContainerShape,
19
19
  } from "../shape.js"
20
20
  import { TypedRef, type TypedRefParams } from "./base.js"
21
21
  import { CounterRef } from "./counter.js"
22
22
  import { ListRef } from "./list.js"
23
- import { MapRef } from "./map.js"
24
23
  import { MovableListRef } from "./movable-list.js"
25
24
  import {
26
25
  listProxyHandler,
@@ -28,6 +27,7 @@ import {
28
27
  recordProxyHandler,
29
28
  } from "./proxy-handlers.js"
30
29
  import { RecordRef } from "./record.js"
30
+ import { StructRef } from "./struct.js"
31
31
  import { TextRef } from "./text.js"
32
32
  import { TreeRef } from "./tree.js"
33
33
 
@@ -38,9 +38,9 @@ import { TreeRef } from "./tree.js"
38
38
  export const containerConstructor = {
39
39
  counter: LoroCounter,
40
40
  list: LoroList,
41
- map: LoroMap,
42
41
  movableList: LoroMovableList,
43
42
  record: LoroMap, // Records use LoroMap as their underlying container
43
+ struct: LoroMap, // Structs use LoroMap as their underlying container
44
44
  text: LoroText,
45
45
  tree: LoroTree,
46
46
  } as const
@@ -124,8 +124,8 @@ export function createContainerTypedRef(
124
124
  new ListRef(params as TypedRefParams<ListContainerShape>),
125
125
  listProxyHandler,
126
126
  )
127
- case "map":
128
- return new MapRef(params as TypedRefParams<MapContainerShape>)
127
+ case "struct":
128
+ return new StructRef(params as TypedRefParams<StructContainerShape>)
129
129
  case "movableList":
130
130
  return new Proxy(
131
131
  new MovableListRef(params as TypedRefParams<MovableListContainerShape>),
@@ -153,7 +153,7 @@ export function assignPlainValueToTypedRef(
153
153
  ): boolean {
154
154
  const shapeType = (ref as any).shape._type
155
155
 
156
- if (shapeType === "map" || shapeType === "record") {
156
+ if (shapeType === "struct" || shapeType === "record") {
157
157
  for (const k in value) {
158
158
  ;(ref as any)[k] = value[k]
159
159
  }
@@ -173,5 +173,27 @@ export function assignPlainValueToTypedRef(
173
173
  }
174
174
  }
175
175
 
176
+ if (shapeType === "text") {
177
+ if (typeof value === "string") {
178
+ ;(ref as any).update(value)
179
+ return true
180
+ }
181
+ return false
182
+ }
183
+
184
+ if (shapeType === "counter") {
185
+ if (typeof value === "number") {
186
+ const currentValue = (ref as any).value
187
+ const diff = value - currentValue
188
+ if (diff > 0) {
189
+ ;(ref as any).increment(diff)
190
+ } else if (diff < 0) {
191
+ ;(ref as any).decrement(-diff)
192
+ }
193
+ return true
194
+ }
195
+ return false
196
+ }
197
+
176
198
  return false
177
199
  }
package/src/types.test.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { describe, expect, expectTypeOf, it } from "vitest"
2
+ import { change } from "./functional-helpers.js"
2
3
  import type { ContainerShape, ValueShape } from "./shape.js"
3
4
  import { Shape } from "./shape.js"
4
5
  import { createTypedDoc } from "./typed-doc.js"
5
- import type { DeepReadonly, Infer } from "./types.js"
6
+ import type { Infer } from "./types.js"
6
7
 
7
8
  describe("Infer type helper", () => {
8
9
  it("infers DocShape plain type", () => {
@@ -23,7 +24,7 @@ describe("Infer type helper", () => {
23
24
  })
24
25
 
25
26
  it("infers ValueShape plain type (object)", () => {
26
- const schema = Shape.plain.object({
27
+ const schema = Shape.plain.struct({
27
28
  name: Shape.plain.string(),
28
29
  age: Shape.plain.number(),
29
30
  })
@@ -35,10 +36,10 @@ describe("Infer type helper", () => {
35
36
  it("infers nested shapes", () => {
36
37
  const schema = Shape.doc({
37
38
  users: Shape.list(
38
- Shape.map({
39
+ Shape.struct({
39
40
  id: Shape.plain.string(),
40
41
  profile: Shape.record(
41
- Shape.plain.object({
42
+ Shape.plain.struct({
42
43
  bio: Shape.plain.string(),
43
44
  }),
44
45
  ),
@@ -57,18 +58,18 @@ describe("Infer type helper", () => {
57
58
 
58
59
  it("infers discriminated union plain type", () => {
59
60
  const SessionStatusSchema = Shape.plain.discriminatedUnion("status", {
60
- not_started: Shape.plain.object({
61
+ not_started: Shape.plain.struct({
61
62
  status: Shape.plain.string("not_started"),
62
63
  }),
63
- lobby: Shape.plain.object({
64
+ lobby: Shape.plain.struct({
64
65
  status: Shape.plain.string("lobby"),
65
66
  lobbyPhase: Shape.plain.string("preparing", "typing"),
66
67
  }),
67
- active: Shape.plain.object({
68
+ active: Shape.plain.struct({
68
69
  status: Shape.plain.string("active"),
69
70
  mode: Shape.plain.string("solo", "group"),
70
71
  }),
71
- paused: Shape.plain.object({
72
+ paused: Shape.plain.struct({
72
73
  status: Shape.plain.string("paused"),
73
74
  previousStatus: Shape.plain.string("lobby", "active"),
74
75
  previousMode: Shape.plain.string("solo", "group"),
@@ -78,7 +79,7 @@ describe("Infer type helper", () => {
78
79
  "assignment_empty",
79
80
  ),
80
81
  }),
81
- ended: Shape.plain.object({
82
+ ended: Shape.plain.struct({
82
83
  status: Shape.plain.string<"ended">("ended"),
83
84
  }),
84
85
  })
@@ -103,16 +104,16 @@ describe("Infer type helper", () => {
103
104
 
104
105
  it("infers discriminated union inside a map container", () => {
105
106
  const SessionStatusSchema = Shape.plain.discriminatedUnion("status", {
106
- not_started: Shape.plain.object({
107
+ not_started: Shape.plain.struct({
107
108
  status: Shape.plain.string("not_started"),
108
109
  }),
109
- active: Shape.plain.object({
110
+ active: Shape.plain.struct({
110
111
  status: Shape.plain.string("active"),
111
112
  mode: Shape.plain.string("solo", "group"),
112
113
  }),
113
114
  })
114
115
 
115
- const SessionMetadataSchema = Shape.map({
116
+ const SessionMetadataSchema = Shape.struct({
116
117
  sessionStartedAt: Shape.plain.number(),
117
118
  sessionStatus: SessionStatusSchema,
118
119
  })
@@ -131,16 +132,16 @@ describe("Infer type helper", () => {
131
132
 
132
133
  it("infers discriminated union inside a doc", () => {
133
134
  const SessionStatusSchema = Shape.plain.discriminatedUnion("status", {
134
- not_started: Shape.plain.object({
135
+ not_started: Shape.plain.struct({
135
136
  status: Shape.plain.string("not_started"),
136
137
  }),
137
- active: Shape.plain.object({
138
+ active: Shape.plain.struct({
138
139
  status: Shape.plain.string("active"),
139
140
  }),
140
141
  })
141
142
 
142
143
  const DocSchema = Shape.doc({
143
- metadata: Shape.map({
144
+ metadata: Shape.struct({
144
145
  sessionStartedAt: Shape.plain.number(),
145
146
  sessionStatus: SessionStatusSchema,
146
147
  }),
@@ -162,11 +163,11 @@ describe("Infer type helper", () => {
162
163
  // This test verifies the fix for usePresence type inference
163
164
  // The issue was that DiscriminatedUnionValueShape<any, any> in the ValueShape union
164
165
  // caused type information to be lost when inferring through generic constraints
165
- const ClientPresenceShape = Shape.plain.object({
166
+ const ClientPresenceShape = Shape.plain.struct({
166
167
  type: Shape.plain.string("client"),
167
168
  name: Shape.plain.string(),
168
169
  })
169
- const ServerPresenceShape = Shape.plain.object({
170
+ const ServerPresenceShape = Shape.plain.struct({
170
171
  type: Shape.plain.string("server"),
171
172
  tick: Shape.plain.number(),
172
173
  })
@@ -188,9 +189,9 @@ describe("Infer type helper", () => {
188
189
  })
189
190
  })
190
191
 
191
- describe("DeepReadonly type helper", () => {
192
- it("Object.values returns clean types without toJSON function in union", () => {
193
- const ParticipantSchema = Shape.plain.object({
192
+ describe("Mutable type helper", () => {
193
+ it("Object.values returns values from the record", () => {
194
+ const ParticipantSchema = Shape.plain.struct({
194
195
  id: Shape.plain.string(),
195
196
  name: Shape.plain.string(),
196
197
  })
@@ -201,29 +202,23 @@ describe("DeepReadonly type helper", () => {
201
202
 
202
203
  const doc = createTypedDoc(GroupSessionSchema)
203
204
 
204
- doc.change((root: any) => {
205
+ change(doc, (root: any) => {
205
206
  root.participants.set("p1", { id: "1", name: "Alice" })
206
207
  root.participants.set("p2", { id: "2", name: "Bob" })
207
208
  })
208
209
 
209
- const participants = doc.value.participants
210
+ const participants = doc.participants
210
211
 
211
- // Object.values should return clean types
212
+ // Object.values returns the values from the record
212
213
  const values = Object.values(participants)
213
214
 
214
- type Participant = Infer<typeof ParticipantSchema>
215
-
216
- // FIXED: Object.values now returns clean DeepReadonly<Participant>[]
217
- // Previously it returned: (DeepReadonly<Participant> | (() => Record<...>))[]
218
- expectTypeOf(values).toEqualTypeOf<DeepReadonly<Participant>[]>()
219
-
220
- // Runtime check
215
+ // Runtime check - values are the plain objects
221
216
  expect(values).toHaveLength(2)
222
- expect(values.map(p => p.name).sort()).toEqual(["Alice", "Bob"])
217
+ expect(values.map((p: any) => p.name).sort()).toEqual(["Alice", "Bob"])
223
218
  })
224
219
 
225
- it("toJSON is still callable on Records", () => {
226
- const ParticipantSchema = Shape.plain.object({
220
+ it("toJSON is callable on Records", () => {
221
+ const ParticipantSchema = Shape.plain.struct({
227
222
  id: Shape.plain.string(),
228
223
  name: Shape.plain.string(),
229
224
  })
@@ -234,11 +229,11 @@ describe("DeepReadonly type helper", () => {
234
229
 
235
230
  const doc = createTypedDoc(GroupSessionSchema)
236
231
 
237
- doc.change((root: any) => {
232
+ change(doc, (root: any) => {
238
233
  root.participants.set("p1", { id: "1", name: "Alice" })
239
234
  })
240
235
 
241
- const participants = doc.value.participants
236
+ const participants = doc.participants
242
237
 
243
238
  // toJSON should be callable
244
239
  const json = participants.toJSON()
@@ -252,8 +247,8 @@ describe("DeepReadonly type helper", () => {
252
247
  expect(json).toEqual({ p1: { id: "1", name: "Alice" } })
253
248
  })
254
249
 
255
- it("toJSON is still callable on Maps", () => {
256
- const MetaSchema = Shape.map({
250
+ it("toJSON is callable on Maps", () => {
251
+ const MetaSchema = Shape.struct({
257
252
  title: Shape.plain.string(),
258
253
  count: Shape.plain.number(),
259
254
  })
@@ -264,12 +259,12 @@ describe("DeepReadonly type helper", () => {
264
259
 
265
260
  const doc = createTypedDoc(DocSchema)
266
261
 
267
- doc.change((root: any) => {
262
+ change(doc, (root: any) => {
268
263
  root.meta.title = "Test"
269
264
  root.meta.count = 42
270
265
  })
271
266
 
272
- const meta = doc.value.meta
267
+ const meta = doc.meta
273
268
 
274
269
  // toJSON should be callable
275
270
  const json = meta.toJSON()
package/src/types.ts CHANGED
@@ -14,7 +14,7 @@ import type { ContainerShape, DocShape, Shape } from "./shape.js"
14
14
  * @example
15
15
  * ```typescript
16
16
  * const ChatSchema = Shape.doc({
17
- * messages: Shape.list(Shape.map({
17
+ * messages: Shape.list(Shape.struct({
18
18
  * id: Shape.plain.string(),
19
19
  * content: Shape.text(),
20
20
  * })),
@@ -24,9 +24,9 @@ import type { ContainerShape, DocShape, Shape } from "./shape.js"
24
24
  * type ChatDoc = Infer<typeof ChatSchema>
25
25
  * // Result: { messages: { id: string; content: string }[] }
26
26
  *
27
- * const PresenceSchema = Shape.plain.object({
27
+ * const PresenceSchema = Shape.plain.struct({
28
28
  * name: Shape.plain.string(),
29
- * cursor: Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() }),
29
+ * cursor: Shape.plain.struct({ x: Shape.plain.number(), y: Shape.plain.number() }),
30
30
  * })
31
31
  *
32
32
  * // Extract the presence type
@@ -58,14 +58,14 @@ export type InferPlaceholderType<T> = T extends Shape<any, any, infer P>
58
58
  : never
59
59
 
60
60
  /**
61
- * Mutable type for use within change() callbacks.
61
+ * Mutable type for use within change() callbacks and direct mutations on doc.value.
62
62
  * This is the type-safe wrapper around CRDT containers that allows mutation.
63
63
  */
64
64
  export type Mutable<T extends DocShape<Record<string, ContainerShape>>> =
65
65
  InferMutableType<T>
66
66
 
67
67
  /**
68
- * @deprecated Use Mutable<T> instead
68
+ * @deprecated Use Mutable<T> instead. Draft is an alias kept for backwards compatibility.
69
69
  */
70
70
  export type Draft<T extends DocShape<Record<string, ContainerShape>>> =
71
71
  Mutable<T>
@@ -77,38 +77,3 @@ export type Draft<T extends DocShape<Record<string, ContainerShape>>> =
77
77
  export interface HasToJSON<T> {
78
78
  toJSON(): T
79
79
  }
80
-
81
- /**
82
- * Deep readonly wrapper for plain objects (no index signature).
83
- * Includes toJSON() method.
84
- */
85
- export type DeepReadonlyObject<T extends object> = {
86
- readonly [P in keyof T]: DeepReadonly<T[P]>
87
- } & HasToJSON<T>
88
-
89
- /**
90
- * Deep readonly wrapper for Record types (with string index signature).
91
- * The toJSON() method is available but NOT part of the index signature,
92
- * so Object.values() returns clean types.
93
- */
94
- export type DeepReadonlyRecord<T> = {
95
- readonly [K in keyof T]: DeepReadonly<T[K]>
96
- } & HasToJSON<Record<string, T[keyof T]>>
97
-
98
- /**
99
- * Deep readonly wrapper that makes all properties readonly recursively
100
- * and adds a toJSON() method for JSON serialization.
101
- *
102
- * For arrays: Returns ReadonlyArray with toJSON()
103
- * For objects with string index signature (Records): toJSON() is available
104
- * but doesn't pollute Object.values() type inference
105
- * For plain objects: Returns readonly properties with toJSON()
106
- * For primitives: Returns as-is
107
- */
108
- export type DeepReadonly<T> = T extends any[]
109
- ? ReadonlyArray<DeepReadonly<T[number]>> & HasToJSON<T>
110
- : T extends object
111
- ? string extends keyof T
112
- ? DeepReadonlyRecord<T>
113
- : DeepReadonlyObject<T>
114
- : T
@@ -14,9 +14,9 @@ import type {
14
14
  ContainerShape,
15
15
  CounterContainerShape,
16
16
  ListContainerShape,
17
- MapContainerShape,
18
17
  MovableListContainerShape,
19
18
  RecordContainerShape,
19
+ StructContainerShape,
20
20
  TextContainerShape,
21
21
  TreeContainerShape,
22
22
  ValueShape,
@@ -146,14 +146,19 @@ export function isMovableListShape(
146
146
  }
147
147
 
148
148
  /**
149
- * Type guard to check if a schema is for MapDraftNode
149
+ * Type guard to check if a schema is for StructDraftNode
150
150
  */
151
- export function isMapShape(
151
+ export function isStructShape(
152
152
  schema: ContainerOrValueShape,
153
- ): schema is MapContainerShape {
154
- return schema && typeof schema === "object" && schema._type === "map"
153
+ ): schema is StructContainerShape {
154
+ return schema && typeof schema === "object" && schema._type === "struct"
155
155
  }
156
156
 
157
+ /**
158
+ * @deprecated Use isStructShape instead. isMapShape is an alias for backward compatibility.
159
+ */
160
+ export const isMapShape = isStructShape
161
+
157
162
  /**
158
163
  * Type guard to check if a schema is for RecordDraftNode
159
164
  */
@@ -193,7 +198,7 @@ export function isValueShape(
193
198
  "null",
194
199
  "undefined",
195
200
  "uint8array",
196
- "object",
201
+ "struct",
197
202
  "record",
198
203
  "array",
199
204
  "union",
package/src/validation.ts CHANGED
@@ -4,12 +4,12 @@ import type {
4
4
  DiscriminatedUnionValueShape,
5
5
  DocShape,
6
6
  ListContainerShape,
7
- MapContainerShape,
8
7
  MovableListContainerShape,
9
- ObjectValueShape,
10
8
  RecordContainerShape,
11
9
  RecordValueShape,
12
10
  StringValueShape,
11
+ StructContainerShape,
12
+ StructValueShape,
13
13
  UnionValueShape,
14
14
  ValueShape,
15
15
  } from "./shape.js"
@@ -60,17 +60,17 @@ export function validateValue(
60
60
  )
61
61
  }
62
62
 
63
- if (schema._type === "map") {
63
+ if (schema._type === "struct") {
64
64
  if (!value || typeof value !== "object" || Array.isArray(value)) {
65
65
  throw new Error(
66
66
  `Expected object at path ${currentPath}, got ${typeof value}`,
67
67
  )
68
68
  }
69
- const mapSchema = schema as MapContainerShape
69
+ const structSchema = schema as StructContainerShape
70
70
  const result: Record<string, unknown> = {}
71
71
 
72
- // Validate each property in the map shape
73
- for (const [key, nestedSchema] of Object.entries(mapSchema.shapes)) {
72
+ // Validate each property in the struct shape
73
+ for (const [key, nestedSchema] of Object.entries(structSchema.shapes)) {
74
74
  const nestedPath = `${currentPath}.${key}`
75
75
  const nestedValue = (value as Record<string, unknown>)[key]
76
76
  result[key] = validateValue(nestedValue, nestedSchema, nestedPath)
@@ -165,17 +165,17 @@ export function validateValue(
165
165
  }
166
166
  return value
167
167
 
168
- case "object": {
168
+ case "struct": {
169
169
  if (!value || typeof value !== "object" || Array.isArray(value)) {
170
170
  throw new Error(
171
171
  `Expected object at path ${currentPath}, got ${typeof value}`,
172
172
  )
173
173
  }
174
- const objectSchema = valueSchema as ObjectValueShape
174
+ const structSchema = valueSchema as StructValueShape
175
175
  const result: Record<string, unknown> = {}
176
176
 
177
- // Validate each property in the object shape
178
- for (const [key, nestedSchema] of Object.entries(objectSchema.shape)) {
177
+ // Validate each property in the struct shape
178
+ for (const [key, nestedSchema] of Object.entries(structSchema.shape)) {
179
179
  const nestedPath = `${currentPath}.${key}`
180
180
  const nestedValue = (value as Record<string, unknown>)[key]
181
181
  result[key] = validateValue(nestedValue, nestedSchema, nestedPath)